From 94a6194044b2db1afcc97f972acfea13060eb151 Mon Sep 17 00:00:00 2001
From: NamPhan
Date: Mon, 17 Aug 2026 16:13:04 +0700
Subject: [PATCH] =?UTF-8?q?feat(hive):=20add=20Flow=20Studio=20and=20Agent?=
=?UTF-8?q?=20Studio=20(Buzz=20Hive=20P0=E2=80=93P5)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Ship buzz-flow and buzz-agent-studio with Nostr kinds 462xx/472xx, pgvector
migration 0032, relay HTTP surfaces and projector, WF-08 approval suspend/resume,
desktop canvas and agent graph UIs, session/block cost telemetry, and docs/E2E smoke.
Signed-off-by: NamPhan
---
ARCHITECTURE.md | 22 +-
Cargo.lock | 25 +
Cargo.toml | 4 +
NOTICE | 22 +
VISION.md | 15 +-
crates/buzz-acp/Cargo.toml | 1 +
crates/buzz-acp/src/pool.rs | 72 +++
crates/buzz-agent-studio/Cargo.toml | 14 +
crates/buzz-agent-studio/src/events.rs | 110 ++++
crates/buzz-agent-studio/src/graph.rs | 300 ++++++++++
crates/buzz-agent-studio/src/graph_events.rs | 82 +++
crates/buzz-agent-studio/src/graph_loader.rs | 93 ++++
crates/buzz-agent-studio/src/lib.rs | 13 +
crates/buzz-agent-studio/src/monitor.rs | 84 +++
crates/buzz-agent-studio/src/skill_import.rs | 96 ++++
crates/buzz-core/src/kind.rs | 76 ++-
crates/buzz-db/src/flow_studio.rs | 516 ++++++++++++++++++
crates/buzz-db/src/lib.rs | 156 ++++++
crates/buzz-db/src/migration.rs | 18 +-
crates/buzz-flow/Cargo.toml | 18 +
crates/buzz-flow/src/blocks/mod.rs | 85 +++
crates/buzz-flow/src/event_payloads.rs | 134 +++++
crates/buzz-flow/src/events.rs | 44 ++
crates/buzz-flow/src/files.rs | 74 +++
crates/buzz-flow/src/knowledge/embed.rs | 80 +++
crates/buzz-flow/src/knowledge/mod.rs | 68 +++
crates/buzz-flow/src/lib.rs | 16 +
crates/buzz-flow/src/projector.rs | 132 +++++
crates/buzz-flow/src/tables.rs | 51 ++
crates/buzz-flow/src/tools/mod.rs | 50 ++
crates/buzz-flow/src/workflow_bridge.rs | 149 +++++
crates/buzz-relay/Cargo.toml | 2 +
crates/buzz-relay/src/api/agent_studio.rs | 246 +++++++++
crates/buzz-relay/src/api/bridge.rs | 4 +
crates/buzz-relay/src/api/flow_studio.rs | 325 +++++++++++
crates/buzz-relay/src/api/mod.rs | 2 +
crates/buzz-relay/src/flow_telemetry.rs | 157 ++++++
.../src/handlers/command_executor.rs | 22 +-
crates/buzz-relay/src/handlers/event.rs | 121 ++++
crates/buzz-relay/src/handlers/ingest.rs | 5 +
crates/buzz-relay/src/lib.rs | 2 +
crates/buzz-relay/src/router.rs | 31 ++
crates/buzz-workflow/src/executor.rs | 53 +-
crates/buzz-workflow/src/lib.rs | 73 ++-
crates/buzz-workflow/src/schema.rs | 6 +
desktop/package.json | 1 +
desktop/playwright.config.ts | 1 +
desktop/src-tauri/src/commands/hive_studio.rs | 153 ++++++
desktop/src-tauri/src/commands/mod.rs | 2 +
.../src/commands/personas/pending.rs | 33 +-
desktop/src-tauri/src/events.rs | 25 +-
desktop/src-tauri/src/events/canvas.rs | 27 +
desktop/src-tauri/src/events/hive_studio.rs | 142 +++++
desktop/src-tauri/src/lib.rs | 17 +-
.../src/managed_agents/agent_studio_events.rs | 35 ++
desktop/src-tauri/src/managed_agents/mod.rs | 1 +
desktop/src/app/AppShell.helpers.ts | 18 +-
desktop/src/app/AppShell.tsx | 4 +
.../src/app/navigation/useAppNavigation.ts | 14 +
desktop/src/app/routeTree.gen.ts | 42 ++
desktop/src/app/routes.ts | 2 +
desktop/src/app/routes/agent-studio.tsx | 23 +
desktop/src/app/routes/flow-studio.tsx | 23 +
.../features/agent-studio/ui/AgentGraph.tsx | 81 +++
.../agent-studio/ui/AgentStudioScreen.tsx | 9 +
.../agent-studio/ui/AgentStudioView.tsx | 127 +++++
.../agent-studio/ui/SessionMonitor.tsx | 123 +++++
.../agent-studio/ui/SkillImportModal.tsx | 127 +++++
.../agent-studio/ui/UnifiedCostMonitor.tsx | 116 ++++
.../features/flow-studio/ui/BlockPalette.tsx | 40 ++
.../src/features/flow-studio/ui/Canvas.tsx | 133 +++++
.../features/flow-studio/ui/FilesPanel.tsx | 116 ++++
.../flow-studio/ui/FlowStudioScreen.tsx | 9 +
.../flow-studio/ui/FlowStudioView.tsx | 265 +++++++++
.../flow-studio/ui/KnowledgeBasePanel.tsx | 120 ++++
.../features/flow-studio/ui/TablesPanel.tsx | 124 +++++
.../src/features/sidebar/ui/AppSidebar.tsx | 16 +-
.../sidebar/ui/AppSidebarPinnedHeader.tsx | 48 +-
.../workflows/ui/WorkflowApprovalCard.tsx | 35 +-
.../workflows/ui/WorkflowRunTrace.tsx | 1 +
desktop/src/shared/api/tauriHiveStudio.ts | 80 +++
desktop/src/shared/constants/kinds.ts | 10 +
desktop/src/shared/ui/ViewLoadingFallback.tsx | 6 +-
desktop/tests/e2e/hive-studio.spec.ts | 69 +++
docker-compose.harness.yml | 2 +-
docker-compose.yml | 2 +-
docs/BUZZ_HIVE_IMPLEMENTATION_PLAN.md | 225 ++++++++
docs/BUZZ_HIVE_MERGE_SPEC.md | 225 ++++++++
docs/DB_AUDIT.md | 42 ++
docs/MERGE_NOTES.md | 50 ++
docs/WF-08.md | 45 ++
migrations/0032_buzz_hive_studio.sql | 61 +++
pnpm-lock.yaml | 184 +++++++
preview-features.json | 12 +
schema/schema.sql | 61 +++
95 files changed, 6723 insertions(+), 73 deletions(-)
create mode 100644 NOTICE
create mode 100644 crates/buzz-agent-studio/Cargo.toml
create mode 100644 crates/buzz-agent-studio/src/events.rs
create mode 100644 crates/buzz-agent-studio/src/graph.rs
create mode 100644 crates/buzz-agent-studio/src/graph_events.rs
create mode 100644 crates/buzz-agent-studio/src/graph_loader.rs
create mode 100644 crates/buzz-agent-studio/src/lib.rs
create mode 100644 crates/buzz-agent-studio/src/monitor.rs
create mode 100644 crates/buzz-agent-studio/src/skill_import.rs
create mode 100644 crates/buzz-db/src/flow_studio.rs
create mode 100644 crates/buzz-flow/Cargo.toml
create mode 100644 crates/buzz-flow/src/blocks/mod.rs
create mode 100644 crates/buzz-flow/src/event_payloads.rs
create mode 100644 crates/buzz-flow/src/events.rs
create mode 100644 crates/buzz-flow/src/files.rs
create mode 100644 crates/buzz-flow/src/knowledge/embed.rs
create mode 100644 crates/buzz-flow/src/knowledge/mod.rs
create mode 100644 crates/buzz-flow/src/lib.rs
create mode 100644 crates/buzz-flow/src/projector.rs
create mode 100644 crates/buzz-flow/src/tables.rs
create mode 100644 crates/buzz-flow/src/tools/mod.rs
create mode 100644 crates/buzz-flow/src/workflow_bridge.rs
create mode 100644 crates/buzz-relay/src/api/agent_studio.rs
create mode 100644 crates/buzz-relay/src/api/flow_studio.rs
create mode 100644 crates/buzz-relay/src/flow_telemetry.rs
create mode 100644 desktop/src-tauri/src/commands/hive_studio.rs
create mode 100644 desktop/src-tauri/src/events/canvas.rs
create mode 100644 desktop/src-tauri/src/events/hive_studio.rs
create mode 100644 desktop/src-tauri/src/managed_agents/agent_studio_events.rs
create mode 100644 desktop/src/app/routes/agent-studio.tsx
create mode 100644 desktop/src/app/routes/flow-studio.tsx
create mode 100644 desktop/src/features/agent-studio/ui/AgentGraph.tsx
create mode 100644 desktop/src/features/agent-studio/ui/AgentStudioScreen.tsx
create mode 100644 desktop/src/features/agent-studio/ui/AgentStudioView.tsx
create mode 100644 desktop/src/features/agent-studio/ui/SessionMonitor.tsx
create mode 100644 desktop/src/features/agent-studio/ui/SkillImportModal.tsx
create mode 100644 desktop/src/features/agent-studio/ui/UnifiedCostMonitor.tsx
create mode 100644 desktop/src/features/flow-studio/ui/BlockPalette.tsx
create mode 100644 desktop/src/features/flow-studio/ui/Canvas.tsx
create mode 100644 desktop/src/features/flow-studio/ui/FilesPanel.tsx
create mode 100644 desktop/src/features/flow-studio/ui/FlowStudioScreen.tsx
create mode 100644 desktop/src/features/flow-studio/ui/FlowStudioView.tsx
create mode 100644 desktop/src/features/flow-studio/ui/KnowledgeBasePanel.tsx
create mode 100644 desktop/src/features/flow-studio/ui/TablesPanel.tsx
create mode 100644 desktop/src/shared/api/tauriHiveStudio.ts
create mode 100644 desktop/tests/e2e/hive-studio.spec.ts
create mode 100644 docs/BUZZ_HIVE_IMPLEMENTATION_PLAN.md
create mode 100644 docs/BUZZ_HIVE_MERGE_SPEC.md
create mode 100644 docs/DB_AUDIT.md
create mode 100644 docs/MERGE_NOTES.md
create mode 100644 docs/WF-08.md
create mode 100644 migrations/0032_buzz_hive_studio.sql
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 892082d96c6..00a8cca616f 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -82,7 +82,9 @@ buzz-core (zero I/O — types, verification, filter matching, kind registry)
├── buzz-pubsub (Redis pub/sub, presence, typing indicators)
├── buzz-search (Postgres FTS: query, delete)
├── buzz-audit (hash-chain tamper-evident log)
- └── buzz-workflow (YAML-as-code automation engine)
+ ├── buzz-workflow (YAML-as-code automation engine)
+ ├── buzz-flow (Flow Studio — canvas blocks, KB, tables, files; kinds 46200–46399)
+ └── buzz-agent-studio (Agent Studio — graph, skills, telemetry; kinds 47200–47399)
│
└── buzz-relay (ties everything together — the server)
@@ -506,6 +508,24 @@ Tamper-evident append-only log with SHA-256 hash chaining.
---
+### buzz-flow — Flow Studio (Buzz Hive)
+
+Visual workflow canvas, block/tool registry, knowledge base, tables, and file metadata. All writes are Nostr events (kinds 46200–46399); Postgres tables in migration `0032` are a read-model projector target.
+
+**HTTP surface (relay):** `/flow-studio/blocks`, `/flow-studio/graph`, `/flow-studio/knowledge/search`, `/flow-studio/tables/{id}/rows`, `/flow-studio/files`, YAML export from canvas.
+
+**Projector:** `buzz-flow/src/projector.rs` — ingest hook in `buzz-relay` applies KB docs, table rows, and file metadata to Postgres.
+
+---
+
+### buzz-agent-studio — Agent Studio (Buzz Hive)
+
+Agent/skill dependency graph, GitHub skill import planning, and ACP session telemetry (kind 47300). Persona publishes also emit kind 47200/47201.
+
+**HTTP surface:** `/agent-studio/graph`, `/agent-studio/sessions`, `/agent-studio/costs`, `/agent-studio/skills/import`.
+
+---
+
### buzz-workflow — YAML-as-Code Automation Engine
Parses, validates, and executes channel-scoped workflow definitions. In multi-community mode workflow definitions, runs, approvals, webhook routes, and schedules inherit the host-derived community and evaluate triggers only against events in that community.
diff --git a/Cargo.lock b/Cargo.lock
index 6c46beedf2f..f83e2a8143b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -830,6 +830,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.22.1",
+ "buzz-agent-studio",
"buzz-core",
"buzz-persona",
"buzz-sdk",
@@ -911,6 +912,16 @@ dependencies = [
"webbrowser",
]
+[[package]]
+name = "buzz-agent-studio"
+version = "0.1.0"
+dependencies = [
+ "buzz-core",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
[[package]]
name = "buzz-audit"
version = "0.1.0"
@@ -1119,6 +1130,18 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "buzz-flow"
+version = "0.1.0"
+dependencies = [
+ "buzz-core",
+ "buzz-workflow",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "uuid",
+]
+
[[package]]
name = "buzz-media"
version = "0.1.0"
@@ -1257,6 +1280,7 @@ dependencies = [
"async-trait",
"axum",
"base64 0.22.1",
+ "buzz-agent-studio",
"buzz-audit",
"buzz-auth",
"buzz-conformance",
@@ -1264,6 +1288,7 @@ dependencies = [
"buzz-datastore-tracing",
"buzz-db",
"buzz-deletion",
+ "buzz-flow",
"buzz-media",
"buzz-pubsub",
"buzz-relay-mesh",
diff --git a/Cargo.toml b/Cargo.toml
index 78816ff4827..e7431f74c0e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,8 @@ members = [
"crates/buzz-admin",
"crates/buzz-deletion",
"crates/buzz-workflow",
+ "crates/buzz-flow",
+ "crates/buzz-agent-studio",
"crates/buzz-media",
"crates/buzz-cli",
"crates/buzz-pairing-cli",
@@ -142,6 +144,8 @@ buzz-pubsub = { path = "crates/buzz-pubsub" }
buzz-search = { path = "crates/buzz-search" }
buzz-audit = { path = "crates/buzz-audit" }
buzz-workflow = { path = "crates/buzz-workflow" }
+buzz-flow = { path = "crates/buzz-flow" }
+buzz-agent-studio = { path = "crates/buzz-agent-studio" }
buzz-media = { path = "crates/buzz-media" }
buzz-sdk = { path = "crates/buzz-sdk" }
buzz-ws-client = { path = "crates/buzz-ws-client" }
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 00000000000..728e4d1ad05
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,22 @@
+Buzz
+Copyright contributors to the Buzz project.
+
+Licensed under the Apache License, Version 2.0.
+
+---
+
+## Third-party components (Buzz Hive ports)
+
+### claude-code-cli-ui (Agent Studio)
+
+- Upstream: https://github.com/Ngxba/claude-code-cli-ui
+- License: MIT
+- Ported to: `crates/buzz-agent-studio`, `desktop/src/features/agent-studio`
+- Copyright remains with original authors; see upstream LICENSE.
+
+### Sim (Flow Studio)
+
+- Upstream: https://github.com/simstudioai/sim
+- License: Apache-2.0 (verify upstream LICENSE before release)
+- Ported to: `crates/buzz-flow`, `desktop/src/features/flow-studio`
+- Reference only — not shipped as Next.js runtime.
diff --git a/VISION.md b/VISION.md
index 900e5a9475b..f649e8282bb 100644
--- a/VISION.md
+++ b/VISION.md
@@ -20,6 +20,8 @@ One community is your entire workspace. Work, conversation, agents, automation,
| ✉️ **DMs** | 1:1 and group. Up to 9. | URGENT only |
| 🤖 **Agents** | Directory. Your agents. Job board. | — |
| ⚡ **Workflows** | YAML-as-code automation. Traces. | Approvals only |
+| 🧩 **Flow Studio** | Visual workflow canvas (Buzz Hive). | Approvals only |
+| 🤖 **Agent Studio** | Agent graph, skill import, cost monitor (Buzz Hive). | — |
| 🔍 **Search** | Cmd+K. Instant. Full-text. | — |
*Desktop app supports all seven surfaces today.*
@@ -122,7 +124,18 @@ Relay communities can pool opted-in member hardware into shared AI compute. Exis
Channel-scoped YAML-as-code automation with conditional logic — the feature Slack paywalled for 5 years. Message triggers, reaction triggers, scheduled runs, webhooks. Every step traced. Agents manage workflows through MCP tools.
-Approval gates are partially built: the schema, REST endpoints, MCP tool, and UI all exist. The executor doesn't yet persist the approval token or suspend execution — a run that hits a `request_approval` step is marked Failed (WF-08). The infrastructure is there; the wiring is next.
+Approval gates suspend execution at `request_approval` steps, persist an approval token, and resume after grant — wired end-to-end for Flow Studio human-approval blocks (WF-08).
+
+---
+
+## Buzz Hive (Flow Studio + Agent Studio)
+
+Visual workflow builder and agent tooling on the same Nostr event log as everything else:
+
+- **Flow Studio** — canvas blocks → YAML → `buzz-workflow` runs; knowledge base (pgvector), tables, and files projected to Postgres; block execution emits kind 46201 for cost rollup.
+- **Agent Studio** — persona/skill dependency graph, GitHub skill import, unified session cost monitor (ACP telemetry kind 47300 + flow blocks).
+
+Both surfaces are preview features in the desktop app (`flow-studio`, `agent-studio`). Spec: `docs/BUZZ_HIVE_MERGE_SPEC.md`.
---
diff --git a/crates/buzz-acp/Cargo.toml b/crates/buzz-acp/Cargo.toml
index d047849806f..fb3ac1a52f9 100644
--- a/crates/buzz-acp/Cargo.toml
+++ b/crates/buzz-acp/Cargo.toml
@@ -19,6 +19,7 @@ path = "src/main.rs"
# Internal
buzz-core = { workspace = true }
buzz-sdk = { workspace = true }
+buzz-agent-studio = { workspace = true }
buzz-persona = { path = "../buzz-persona" }
# Nostr
diff --git a/crates/buzz-acp/src/pool.rs b/crates/buzz-acp/src/pool.rs
index 2efacce2b19..e53ca696305 100644
--- a/crates/buzz-acp/src/pool.rs
+++ b/crates/buzz-acp/src/pool.rs
@@ -4177,6 +4177,78 @@ async fn publish_agent_turn_metric(
"NIP-AM: publish timed out"
),
}
+ publish_agent_session_telemetry(ctx, &usage, session_id).await;
+}
+
+/// Best-effort: publish kind 47300 session telemetry for Agent Studio monitor.
+async fn publish_agent_session_telemetry(
+ ctx: &PromptContext,
+ usage: &crate::usage::TurnUsage,
+ session_id: &str,
+) {
+ use buzz_agent_studio::events::AgentSessionTelemetry;
+ use nostr::{EventBuilder, Kind, Tag};
+
+ let Some(owner_pk) = ctx.agent_owner_pubkey.as_ref() else {
+ return;
+ };
+
+ let payload = AgentSessionTelemetry {
+ session_id: session_id.to_string(),
+ agent_id: Some(ctx.harness_name.clone()),
+ input_tokens: usage.cumulative_input_tokens.unwrap_or(0),
+ output_tokens: usage.cumulative_output_tokens.unwrap_or(0),
+ cost_usd: usage.cumulative_cost_usd.unwrap_or(0.0),
+ tool_calls: 0,
+ };
+ let content = match serde_json::to_string(&payload) {
+ Ok(content) => content,
+ Err(error) => {
+ tracing::warn!(
+ target: "pool::metrics",
+ session_id,
+ "Agent Studio telemetry serialize failed: {error}"
+ );
+ return;
+ }
+ };
+ let owner_hex = owner_pk.to_hex();
+ let agent_hex = ctx.agent_keys.public_key().to_hex();
+ let event = match EventBuilder::new(
+ Kind::Custom(buzz_core::kind::KIND_AGENT_SESSION_TELEMETRY as u16),
+ content,
+ )
+ .tags([
+ Tag::parse(["d", session_id]).expect("d tag"),
+ Tag::parse(["p", &owner_hex]).expect("p tag"),
+ Tag::parse(["agent", &agent_hex]).expect("agent tag"),
+ ])
+ .sign_with_keys(&ctx.agent_keys)
+ {
+ Ok(event) => event,
+ Err(error) => {
+ tracing::warn!(
+ target: "pool::metrics",
+ session_id,
+ "Agent Studio telemetry sign failed: {error}"
+ );
+ return;
+ }
+ };
+ const TELEMETRY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
+ match tokio::time::timeout(TELEMETRY_TIMEOUT, ctx.rest_client.submit_event(&event)).await {
+ Ok(Ok(_)) => {}
+ Ok(Err(error)) => tracing::warn!(
+ target: "pool::metrics",
+ session_id,
+ "Agent Studio telemetry publish failed: {error}"
+ ),
+ Err(_) => tracing::warn!(
+ target: "pool::metrics",
+ session_id,
+ "Agent Studio telemetry publish timed out"
+ ),
+ }
}
const REACTION_SEEN: &str = "👀";
diff --git a/crates/buzz-agent-studio/Cargo.toml b/crates/buzz-agent-studio/Cargo.toml
new file mode 100644
index 00000000000..b1a2ac952fd
--- /dev/null
+++ b/crates/buzz-agent-studio/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "buzz-agent-studio"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "Buzz Agent Studio — agent/skill graph ported from claude-code-cli-ui (Buzz Hive P0 skeleton)"
+
+[dependencies]
+buzz-core = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+thiserror = { workspace = true }
diff --git a/crates/buzz-agent-studio/src/events.rs b/crates/buzz-agent-studio/src/events.rs
new file mode 100644
index 00000000000..d602d4f278e
--- /dev/null
+++ b/crates/buzz-agent-studio/src/events.rs
@@ -0,0 +1,110 @@
+//! Nostr event kinds and payloads for Buzz Agent Studio (claude-code-cli-ui merge).
+//!
+//! Kind numbers are defined in `buzz_core::kind` (range 47200–47399).
+
+pub use buzz_core::kind::{
+ is_agent_studio_kind, KIND_AGENT_CONFIG_CREATED, KIND_AGENT_CONFIG_UPDATED,
+ KIND_AGENT_GRAPH_EDGE, KIND_AGENT_SESSION_TELEMETRY, KIND_AGENT_SKILL_IMPORTED,
+};
+
+/// Payload for [`KIND_AGENT_CONFIG_CREATED`].
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+pub struct AgentConfigCreated {
+ /// Persona / agent slug (`d` tag).
+ pub agent_id: String,
+ /// Serialized persona frontmatter or JSON config.
+ pub config_json: String,
+}
+
+/// Payload for [`KIND_AGENT_CONFIG_UPDATED`].
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+pub struct AgentConfigUpdated {
+ /// Persona / agent slug (`d` tag).
+ pub agent_id: String,
+ /// Serialized persona frontmatter or JSON config.
+ pub config_json: String,
+}
+
+/// Payload for [`KIND_AGENT_SESSION_TELEMETRY`].
+#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
+pub struct AgentSessionTelemetry {
+ /// Session identifier.
+ pub session_id: String,
+ /// Agent / persona slug when known.
+ pub agent_id: Option,
+ /// Cumulative input tokens.
+ pub input_tokens: u64,
+ /// Cumulative output tokens.
+ pub output_tokens: u64,
+ /// Estimated USD cost.
+ pub cost_usd: f64,
+ /// Tool invocation count.
+ pub tool_calls: u32,
+}
+
+/// Payload for [`KIND_AGENT_GRAPH_EDGE`].
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+pub struct AgentGraphEdge {
+ /// Source node kind label.
+ pub source_type: String,
+ /// Source slug.
+ pub source_slug: String,
+ /// Target node kind label.
+ pub target_type: String,
+ /// Target slug.
+ pub target_slug: String,
+ /// Edge semantics.
+ pub relationship_type: String,
+ /// Detection evidence.
+ pub evidence: String,
+}
+
+/// Payload for [`KIND_AGENT_SKILL_IMPORTED`].
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+pub struct AgentSkillImported {
+ /// Skill identifier.
+ pub skill_id: String,
+ /// Source repository URL when imported from GitHub.
+ pub source_repo: Option,
+ /// Commit SHA at import time.
+ pub source_commit: Option,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn agent_config_json_roundtrip() {
+ let event = AgentConfigCreated {
+ agent_id: "reviewer".into(),
+ config_json: r#"{"model":"claude-sonnet"}"#.into(),
+ };
+ let json = serde_json::to_string(&event).expect("serialize");
+ let back: AgentConfigCreated = serde_json::from_str(&json).expect("deserialize");
+ assert_eq!(back, event);
+ }
+
+ #[test]
+ fn skill_import_json_roundtrip() {
+ let event = AgentSkillImported {
+ skill_id: "lint-rust".into(),
+ source_repo: Some("https://github.com/example/skills".into()),
+ source_commit: Some("abc123".into()),
+ };
+ let json = serde_json::to_string(&event).expect("serialize");
+ let back: AgentSkillImported = serde_json::from_str(&json).expect("deserialize");
+ assert_eq!(back, event);
+ }
+
+ #[test]
+ fn kind_range_covers_agent_studio() {
+ assert!(is_agent_studio_kind(KIND_AGENT_CONFIG_CREATED));
+ assert!(is_agent_studio_kind(KIND_AGENT_GRAPH_EDGE));
+ assert!(is_agent_studio_kind(47399));
+ assert!(!is_agent_studio_kind(47199));
+ assert!(!is_agent_studio_kind(47400));
+ assert!(!is_agent_studio_kind(48001));
+ assert!(!is_agent_studio_kind(48100));
+ }
+}
diff --git a/crates/buzz-agent-studio/src/graph.rs b/crates/buzz-agent-studio/src/graph.rs
new file mode 100644
index 00000000000..4ec19026b08
--- /dev/null
+++ b/crates/buzz-agent-studio/src/graph.rs
@@ -0,0 +1,300 @@
+//! Dependency graph extraction (ported from claude-code-cli-ui `relationships.ts`).
+
+use serde::{Deserialize, Serialize};
+
+/// Entity type in the agent/command/skill graph.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum GraphNodeType {
+ /// Claude Code agent definition.
+ Agent,
+ /// Slash command.
+ Command,
+ /// Skill pack.
+ Skill,
+ /// MCP server (read-only node).
+ Mcp,
+}
+
+/// Edge semantics between graph nodes.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum RelationshipType {
+ /// Command body spawns or references an agent.
+ Spawns,
+ /// Frontmatter declares a dependency.
+ AgentFrontmatter,
+ /// Inverse spawn reference.
+ SpawnedBy,
+}
+
+/// A directed edge in the agent studio dependency graph.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Relationship {
+ /// Source node kind.
+ pub source_type: GraphNodeType,
+ /// Source slug (agent name, command path, skill id).
+ pub source_slug: String,
+ /// Target node kind.
+ pub target_type: GraphNodeType,
+ /// Target slug.
+ pub target_slug: String,
+ /// How the edge was detected.
+ pub relationship_type: RelationshipType,
+ /// Human-readable evidence (frontmatter key, matched snippet).
+ pub evidence: String,
+}
+
+/// Agent markdown entry scanned from `.claude/agents/*.md`.
+#[derive(Clone, Debug)]
+pub struct AgentEntry {
+ /// Agent slug (filename without `.md`).
+ pub slug: String,
+ /// Markdown body after frontmatter.
+ pub body: String,
+ /// Parsed YAML frontmatter values.
+ pub skills: Vec,
+}
+
+/// Command markdown entry.
+#[derive(Clone, Debug)]
+pub struct CommandEntry {
+ /// Command slug (may include `--` for nested paths).
+ pub slug: String,
+ /// Markdown body.
+ pub body: String,
+ /// Optional `agent:` frontmatter reference.
+ pub agent_ref: Option,
+}
+
+/// Skill entry.
+#[derive(Clone, Debug)]
+pub struct SkillEntry {
+ /// Skill directory name / slug.
+ pub slug: String,
+}
+
+/// Build dependency edges from scanned Claude Code config entries.
+pub fn extract_relationships(
+ agents: &[AgentEntry],
+ commands: &[CommandEntry],
+ skills: &[SkillEntry],
+ extra_skill_slugs: &[String],
+) -> Vec {
+ let agent_names: std::collections::HashSet<&str> =
+ agents.iter().map(|a| a.slug.as_str()).collect();
+ let mut skill_slugs: std::collections::HashSet<&str> =
+ skills.iter().map(|s| s.slug.as_str()).collect();
+ for slug in extra_skill_slugs {
+ skill_slugs.insert(slug.as_str());
+ }
+
+ let mut relationships = Vec::new();
+ let mut seen = std::collections::HashSet::new();
+
+ let mut add = |rel: Relationship| {
+ let key = format!(
+ "{:?}:{}->{:?}:{}",
+ rel.source_type, rel.source_slug, rel.target_type, rel.target_slug
+ );
+ if seen.insert(key) {
+ relationships.push(rel);
+ }
+ };
+
+ for agent in agents {
+ for skill_slug in &agent.skills {
+ if skill_slugs.contains(skill_slug.as_str()) {
+ add(Relationship {
+ source_type: GraphNodeType::Agent,
+ source_slug: agent.slug.clone(),
+ target_type: GraphNodeType::Skill,
+ target_slug: skill_slug.clone(),
+ relationship_type: RelationshipType::AgentFrontmatter,
+ evidence: format!("preloads skill: {skill_slug}"),
+ });
+ }
+ }
+ }
+
+ for cmd in commands {
+ if let Some(agent_ref) = &cmd.agent_ref {
+ if agent_names.contains(agent_ref.as_str()) {
+ add(Relationship {
+ source_type: GraphNodeType::Command,
+ source_slug: cmd.slug.clone(),
+ target_type: GraphNodeType::Agent,
+ target_slug: agent_ref.clone(),
+ relationship_type: RelationshipType::AgentFrontmatter,
+ evidence: format!("agent: {agent_ref}"),
+ });
+ }
+ }
+
+ if cmd.body.to_ascii_lowercase().contains("subagent_type") {
+ if let Some(idx) = cmd.body.to_ascii_lowercase().find("subagent_type") {
+ let tail = &cmd.body[idx..];
+ if let Some(spawned) = parse_quoted_slug_after_colon(tail) {
+ if agent_names.contains(spawned.as_str()) {
+ add(Relationship {
+ source_type: GraphNodeType::Command,
+ source_slug: cmd.slug.clone(),
+ target_type: GraphNodeType::Agent,
+ target_slug: spawned,
+ relationship_type: RelationshipType::Spawns,
+ evidence: "subagent_type reference".into(),
+ });
+ }
+ }
+ }
+ }
+
+ for spawn in find_spawn_patterns(&cmd.body) {
+ if agent_names.contains(spawn.as_str()) {
+ add(Relationship {
+ source_type: GraphNodeType::Command,
+ source_slug: cmd.slug.clone(),
+ target_type: GraphNodeType::Agent,
+ target_slug: spawn,
+ relationship_type: RelationshipType::Spawns,
+ evidence: "spawn pattern".into(),
+ });
+ }
+ }
+ }
+
+ relationships
+}
+
+/// Serialize graph as `{ nodes, edges }` for HTTP clients.
+pub fn graph_json(
+ agents: &[AgentEntry],
+ commands: &[CommandEntry],
+ skills: &[SkillEntry],
+ extra_skill_slugs: &[String],
+) -> serde_json::Value {
+ let edges = extract_relationships(agents, commands, skills, extra_skill_slugs);
+
+ let mut node_ids = std::collections::BTreeSet::new();
+ for agent in agents {
+ node_ids.insert(format!("agent:{}", agent.slug));
+ }
+ for cmd in commands {
+ node_ids.insert(format!("command:{}", cmd.slug));
+ }
+ for skill in skills {
+ node_ids.insert(format!("skill:{}", skill.slug));
+ }
+ for edge in &edges {
+ node_ids.insert(format!(
+ "{}:{}",
+ node_kind_label(&edge.source_type),
+ edge.source_slug
+ ));
+ node_ids.insert(format!(
+ "{}:{}",
+ node_kind_label(&edge.target_type),
+ edge.target_slug
+ ));
+ }
+
+ let nodes: Vec = node_ids
+ .into_iter()
+ .map(|id| {
+ let (kind, slug) = id.split_once(':').unwrap_or((&id, ""));
+ serde_json::json!({ "id": id, "kind": kind, "slug": slug })
+ })
+ .collect();
+
+ serde_json::json!({ "nodes": nodes, "edges": edges })
+}
+
+fn node_kind_label(kind: &GraphNodeType) -> &'static str {
+ match kind {
+ GraphNodeType::Agent => "agent",
+ GraphNodeType::Command => "command",
+ GraphNodeType::Skill => "skill",
+ GraphNodeType::Mcp => "mcp",
+ }
+}
+
+fn parse_quoted_slug_after_colon(text: &str) -> Option {
+ let after = text.split(':').nth(1)?.trim();
+ let slug = after
+ .trim_start_matches(['"', '\''])
+ .split(|c: char| c.is_whitespace() || c == '"' || c == '\'')
+ .next()?
+ .trim();
+ if slug.is_empty() || !slug.starts_with(|c: char| c.is_ascii_lowercase()) {
+ return None;
+ }
+ Some(slug.to_string())
+}
+
+fn find_spawn_patterns(body: &str) -> Vec {
+ let mut found = Vec::new();
+ let lower = body.to_ascii_lowercase();
+ for prefix in ["spawn ", "spawns ", "spawned "] {
+ let mut start = 0;
+ while let Some(idx) = lower[start..].find(prefix) {
+ let abs = start + idx + prefix.len();
+ let tail = body.get(abs..).unwrap_or("");
+ let trimmed = tail
+ .trim_start()
+ .strip_prefix("the ")
+ .or_else(|| tail.trim_start().strip_prefix("The "))
+ .unwrap_or(tail.trim_start());
+ if let Some(slug) = trimmed
+ .trim_start_matches(['"', '\''])
+ .split(|c: char| c.is_whitespace() || c == '"' || c == '\'')
+ .next()
+ {
+ if slug.starts_with(|c: char| c.is_ascii_lowercase()) {
+ found.push(slug.to_string());
+ }
+ }
+ start = abs;
+ }
+ }
+ found
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn agent_skill_frontmatter_edge() {
+ let agents = vec![AgentEntry {
+ slug: "reviewer".into(),
+ body: String::new(),
+ skills: vec!["lint".into()],
+ }];
+ let skills = vec![SkillEntry {
+ slug: "lint".into(),
+ }];
+ let edges = extract_relationships(&agents, &[], &skills, &[]);
+ assert_eq!(edges.len(), 1);
+ assert_eq!(edges[0].target_slug, "lint");
+ }
+
+ #[test]
+ fn command_agent_frontmatter_edge() {
+ let agents = vec![AgentEntry {
+ slug: "worker".into(),
+ body: String::new(),
+ skills: vec![],
+ }];
+ let commands = vec![CommandEntry {
+ slug: "ship".into(),
+ body: String::new(),
+ agent_ref: Some("worker".into()),
+ }];
+ let edges = extract_relationships(&agents, &commands, &[], &[]);
+ assert_eq!(edges.len(), 1);
+ assert_eq!(
+ edges[0].relationship_type,
+ RelationshipType::AgentFrontmatter
+ );
+ }
+}
diff --git a/crates/buzz-agent-studio/src/graph_events.rs b/crates/buzz-agent-studio/src/graph_events.rs
new file mode 100644
index 00000000000..ee53f54761d
--- /dev/null
+++ b/crates/buzz-agent-studio/src/graph_events.rs
@@ -0,0 +1,82 @@
+//! Build graph nodes/edges from Nostr Agent Studio events (kinds 47200+, 47350+).
+
+use crate::events::{AgentConfigCreated, AgentSkillImported};
+use crate::graph::{
+ graph_json, AgentEntry, CommandEntry, GraphNodeType, RelationshipType, SkillEntry,
+};
+
+/// Parsed graph edge from kind 47350 event content.
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
+pub struct GraphEdgeEvent {
+ /// Source node kind.
+ pub source_type: GraphNodeType,
+ /// Source slug.
+ pub source_slug: String,
+ /// Target node kind.
+ pub target_type: GraphNodeType,
+ /// Target slug.
+ pub target_slug: String,
+ /// Edge semantics.
+ pub relationship_type: RelationshipType,
+ /// Detection evidence.
+ pub evidence: String,
+}
+
+/// Merge scanned filesystem entries with persisted Nostr events.
+pub fn graph_from_events(
+ agents: &[AgentEntry],
+ commands: &[CommandEntry],
+ skills: &[SkillEntry],
+ configs: &[AgentConfigCreated],
+ imported_skills: &[AgentSkillImported],
+ edge_events: &[GraphEdgeEvent],
+) -> serde_json::Value {
+ let mut skill_entries: Vec = skills.to_vec();
+ for imported in imported_skills {
+ if !skill_entries.iter().any(|s| s.slug == imported.skill_id) {
+ skill_entries.push(SkillEntry {
+ slug: imported.skill_id.clone(),
+ });
+ }
+ }
+
+ let mut agent_entries: Vec = agents.to_vec();
+ for cfg in configs {
+ if !agent_entries.iter().any(|a| a.slug == cfg.agent_id) {
+ agent_entries.push(AgentEntry {
+ slug: cfg.agent_id.clone(),
+ body: String::new(),
+ skills: vec![],
+ });
+ }
+ }
+
+ let mut extra_skills: Vec = Vec::new();
+ extra_skills.sort();
+ extra_skills.dedup();
+
+ let mut base = graph_json(&agent_entries, commands, &skill_entries, &extra_skills);
+ if let Some(edges) = base.get_mut("edges").and_then(|v| v.as_array_mut()) {
+ for ev in edge_events {
+ edges.push(serde_json::to_value(ev).unwrap_or_default());
+ }
+ }
+ base
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn imported_skills_add_nodes() {
+ let imported = vec![AgentSkillImported {
+ skill_id: "lint-rust".into(),
+ source_repo: None,
+ source_commit: None,
+ }];
+ let graph = graph_from_events(&[], &[], &[], &[], &imported, &[]);
+ let nodes = graph["nodes"].as_array().expect("nodes");
+ assert!(nodes.iter().any(|n| n["slug"] == "lint-rust"));
+ }
+}
diff --git a/crates/buzz-agent-studio/src/graph_loader.rs b/crates/buzz-agent-studio/src/graph_loader.rs
new file mode 100644
index 00000000000..5311cc2d258
--- /dev/null
+++ b/crates/buzz-agent-studio/src/graph_loader.rs
@@ -0,0 +1,93 @@
+//! Parse stored Nostr events into Agent Studio graph JSON.
+
+use buzz_core::kind::{
+ KIND_AGENT_CONFIG_CREATED, KIND_AGENT_CONFIG_UPDATED, KIND_AGENT_GRAPH_EDGE,
+ KIND_AGENT_SKILL_IMPORTED,
+};
+
+use crate::events::{AgentConfigCreated, AgentGraphEdge, AgentSkillImported};
+use crate::graph_events::{graph_from_events, GraphEdgeEvent};
+
+/// Minimal stored event fields needed for graph projection.
+#[derive(Clone, Debug)]
+pub struct StoredAgentStudioEvent {
+ /// Nostr event kind.
+ pub kind: u32,
+ /// Event content JSON.
+ pub content: String,
+}
+
+/// Build `{ nodes, edges }` from relay-stored Agent Studio events.
+pub fn graph_from_stored_events(events: &[StoredAgentStudioEvent]) -> serde_json::Value {
+ let mut configs = Vec::new();
+ let mut imported_skills = Vec::new();
+ let mut edge_events = Vec::new();
+
+ for event in events {
+ match event.kind {
+ KIND_AGENT_CONFIG_CREATED | KIND_AGENT_CONFIG_UPDATED => {
+ if let Ok(payload) = serde_json::from_str::(&event.content) {
+ configs.push(payload);
+ }
+ }
+ KIND_AGENT_SKILL_IMPORTED => {
+ if let Ok(payload) = serde_json::from_str::(&event.content) {
+ imported_skills.push(payload);
+ }
+ }
+ KIND_AGENT_GRAPH_EDGE => {
+ if let Ok(raw) = serde_json::from_str::(&event.content) {
+ edge_events.push(parse_graph_edge(&raw));
+ }
+ }
+ _ => {}
+ }
+ }
+
+ graph_from_events(&[], &[], &[], &configs, &imported_skills, &edge_events)
+}
+
+fn parse_graph_edge(raw: &AgentGraphEdge) -> GraphEdgeEvent {
+ use crate::graph::{GraphNodeType, RelationshipType};
+
+ let parse_kind = |label: &str| -> GraphNodeType {
+ match label {
+ "command" => GraphNodeType::Command,
+ "skill" => GraphNodeType::Skill,
+ "mcp" => GraphNodeType::Mcp,
+ _ => GraphNodeType::Agent,
+ }
+ };
+ let parse_rel = |label: &str| -> RelationshipType {
+ match label {
+ "spawns" => RelationshipType::Spawns,
+ "spawned-by" => RelationshipType::SpawnedBy,
+ _ => RelationshipType::AgentFrontmatter,
+ }
+ };
+
+ GraphEdgeEvent {
+ source_type: parse_kind(&raw.source_type),
+ source_slug: raw.source_slug.clone(),
+ target_type: parse_kind(&raw.target_type),
+ target_slug: raw.target_slug.clone(),
+ relationship_type: parse_rel(&raw.relationship_type),
+ evidence: raw.evidence.clone(),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn config_event_adds_agent_node() {
+ let content = r#"{"agent_id":"reviewer","config_json":"{}"}"#;
+ let graph = graph_from_stored_events(&[StoredAgentStudioEvent {
+ kind: KIND_AGENT_CONFIG_CREATED,
+ content: content.into(),
+ }]);
+ let nodes = graph["nodes"].as_array().expect("nodes");
+ assert!(nodes.iter().any(|n| n["slug"] == "reviewer"));
+ }
+}
diff --git a/crates/buzz-agent-studio/src/lib.rs b/crates/buzz-agent-studio/src/lib.rs
new file mode 100644
index 00000000000..f3500a092e4
--- /dev/null
+++ b/crates/buzz-agent-studio/src/lib.rs
@@ -0,0 +1,13 @@
+#![deny(unsafe_code)]
+#![warn(missing_docs)]
+//! `buzz-agent-studio` — Agent Studio backend for Buzz Hive.
+//!
+//! Port target: [Ngxba/claude-code-cli-ui](https://github.com/Ngxba/claude-code-cli-ui).
+//! Kind registry: `buzz_core::kind` range 47200–47399.
+
+pub mod events;
+pub mod graph;
+pub mod graph_events;
+pub mod graph_loader;
+pub mod monitor;
+pub mod skill_import;
diff --git a/crates/buzz-agent-studio/src/monitor.rs b/crates/buzz-agent-studio/src/monitor.rs
new file mode 100644
index 00000000000..e3d6d98cc0e
--- /dev/null
+++ b/crates/buzz-agent-studio/src/monitor.rs
@@ -0,0 +1,84 @@
+//! ACP session telemetry → Nostr kind 47300 (Session Monitor).
+
+use serde::{Deserialize, Serialize};
+
+/// Single telemetry snapshot for an agent session.
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct SessionTelemetry {
+ /// Session identifier (ACP session id or run id).
+ pub session_id: String,
+ /// Agent / persona slug when known.
+ pub agent_id: Option,
+ /// Cumulative input tokens.
+ pub input_tokens: u64,
+ /// Cumulative output tokens.
+ pub output_tokens: u64,
+ /// Estimated USD cost (community-defined pricing).
+ pub cost_usd: f64,
+ /// Tool invocations in this session.
+ pub tool_calls: u32,
+ /// Unix timestamp (seconds).
+ pub recorded_at: i64,
+}
+
+/// In-memory ring buffer for SSE / polling (MVP).
+#[derive(Clone, Debug, Default)]
+pub struct SessionMonitor {
+ max_entries: usize,
+ entries: Vec,
+}
+
+impl SessionMonitor {
+ /// Create a monitor retaining the last `max_entries` snapshots.
+ pub fn new(max_entries: usize) -> Self {
+ Self {
+ max_entries: max_entries.max(1),
+ entries: Vec::new(),
+ }
+ }
+
+ /// Record a telemetry snapshot.
+ pub fn record(&mut self, telemetry: SessionTelemetry) {
+ if self.entries.len() >= self.max_entries {
+ self.entries.remove(0);
+ }
+ self.entries.push(telemetry);
+ }
+
+ /// Latest snapshots (newest last).
+ pub fn snapshots(&self) -> &[SessionTelemetry] {
+ &self.entries
+ }
+
+ /// Aggregate cost across all retained sessions.
+ pub fn total_cost_usd(&self) -> f64 {
+ self.entries.iter().map(|e| e.cost_usd).sum()
+ }
+}
+
+/// JSON array for HTTP API.
+pub fn telemetry_json(entries: &[SessionTelemetry]) -> serde_json::Value {
+ serde_json::json!({ "sessions": entries })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn monitor_retains_max_entries() {
+ let mut monitor = SessionMonitor::new(2);
+ for i in 0..3 {
+ monitor.record(SessionTelemetry {
+ session_id: format!("s{i}"),
+ agent_id: None,
+ input_tokens: 1,
+ output_tokens: 1,
+ cost_usd: 0.01,
+ tool_calls: 0,
+ recorded_at: i,
+ });
+ }
+ assert_eq!(monitor.snapshots().len(), 2);
+ }
+}
diff --git a/crates/buzz-agent-studio/src/skill_import.rs b/crates/buzz-agent-studio/src/skill_import.rs
new file mode 100644
index 00000000000..60e374205d5
--- /dev/null
+++ b/crates/buzz-agent-studio/src/skill_import.rs
@@ -0,0 +1,96 @@
+//! GitHub skill import (ported from claude-code-cli-ui import workflow).
+
+use crate::events::AgentSkillImported;
+
+/// Parsed GitHub repository reference from user input.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct GithubRepoRef {
+ /// `owner/repo` slug.
+ pub slug: String,
+ /// Optional branch or tag.
+ pub r#ref: Option,
+}
+
+/// Planned skill import before event emission.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct SkillImportPlan {
+ /// Target skill id (directory slug).
+ pub skill_id: String,
+ /// Source repository.
+ pub repo: GithubRepoRef,
+ /// Optional subdirectory within the repo.
+ pub path: Option,
+}
+
+/// Errors during skill import planning.
+#[derive(Debug, thiserror::Error)]
+pub enum SkillImportError {
+ /// URL or slug could not be parsed.
+ #[error("invalid GitHub URL: {0}")]
+ InvalidUrl(String),
+ /// Skill id failed validation.
+ #[error("invalid skill id: {0}")]
+ InvalidSkillId(String),
+}
+
+/// Parse `https://github.com/owner/repo` or `owner/repo` into a repo ref.
+pub fn parse_github_repo(input: &str) -> Result {
+ let trimmed = input.trim().trim_end_matches('/');
+ let slug = trimmed
+ .strip_prefix("https://github.com/")
+ .or_else(|| trimmed.strip_prefix("http://github.com/"))
+ .unwrap_or(trimmed);
+ let parts: Vec<&str> = slug.split('/').filter(|p| !p.is_empty()).collect();
+ if parts.len() < 2 {
+ return Err(SkillImportError::InvalidUrl(input.to_string()));
+ }
+ Ok(GithubRepoRef {
+ slug: format!("{}/{}", parts[0], parts[1]),
+ r#ref: parts.get(2).map(|s| s.to_string()),
+ })
+}
+
+/// Build an import plan from repo URL and skill slug.
+pub fn plan_skill_import(
+ repo_input: &str,
+ skill_id: &str,
+ path: Option<&str>,
+) -> Result {
+ if skill_id.is_empty()
+ || !skill_id
+ .chars()
+ .all(|c| c.is_ascii_alphanumeric() || c == '-')
+ {
+ return Err(SkillImportError::InvalidSkillId(skill_id.to_string()));
+ }
+ Ok(SkillImportPlan {
+ skill_id: skill_id.to_string(),
+ repo: parse_github_repo(repo_input)?,
+ path: path.map(str::to_string),
+ })
+}
+
+/// Convert a successful import plan to a Nostr event payload (kind 47250).
+pub fn import_plan_to_event(plan: &SkillImportPlan, commit: Option<&str>) -> AgentSkillImported {
+ AgentSkillImported {
+ skill_id: plan.skill_id.clone(),
+ source_repo: Some(format!("https://github.com/{}", plan.repo.slug)),
+ source_commit: commit.map(str::to_string),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parse_github_https() {
+ let repo = parse_github_repo("https://github.com/block/buzz").expect("parse");
+ assert_eq!(repo.slug, "block/buzz");
+ }
+
+ #[test]
+ fn plan_rejects_bad_skill_id() {
+ assert!(plan_skill_import("block/buzz", "../bad", None).is_err());
+ }
+}
diff --git a/crates/buzz-core/src/kind.rs b/crates/buzz-core/src/kind.rs
index 3c6f1d5913d..7012cae2490 100644
--- a/crates/buzz-core/src/kind.rs
+++ b/crates/buzz-core/src/kind.rs
@@ -581,7 +581,47 @@ pub const KIND_WORKFLOW_APPROVAL_GRANTED: u32 = 46011;
/// A pending workflow approval was denied.
pub const KIND_WORKFLOW_APPROVAL_DENIED: u32 = 46012;
-// User groups (47000–47999)
+// Flow Studio (46200–46399) — Buzz Hive / Sim visual workflow merge.
+/// Flow graph saved to the event log.
+pub const KIND_FLOW_GRAPH_SAVED: u32 = 46200;
+/// A Flow Studio block started execution.
+pub const KIND_FLOW_BLOCK_EXECUTED: u32 = 46201;
+/// A Flow Studio block failed.
+pub const KIND_FLOW_BLOCK_FAILED: u32 = 46202;
+/// Knowledge base document ingested.
+pub const KIND_FLOW_KB_DOCUMENT_INGESTED: u32 = 46250;
+/// Embedding indexed for semantic search.
+pub const KIND_FLOW_KB_EMBEDDING_INDEXED: u32 = 46251;
+/// Semantic query recorded.
+pub const KIND_FLOW_KB_SEMANTIC_QUERY: u32 = 46252;
+/// Tables row created.
+pub const KIND_FLOW_TABLE_ROW_CREATED: u32 = 46300;
+/// Tables row updated.
+pub const KIND_FLOW_TABLE_ROW_UPDATED: u32 = 46301;
+/// Tables row deleted.
+pub const KIND_FLOW_TABLE_ROW_DELETED: u32 = 46302;
+/// File uploaded via Flow Studio.
+pub const KIND_FLOW_FILE_UPLOADED: u32 = 46350;
+/// File version recorded.
+pub const KIND_FLOW_FILE_VERSIONED: u32 = 46351;
+/// File deleted.
+pub const KIND_FLOW_FILE_DELETED: u32 = 46352;
+
+// Agent Studio (47200–47399) — Buzz Hive / claude-code-cli-ui merge.
+// Occupies the reserved user-groups band (47000–47999); does not overlap
+// audit (48001) or huddle (48100+) kinds.
+/// Agent config created (persona binding).
+pub const KIND_AGENT_CONFIG_CREATED: u32 = 47200;
+/// Agent config updated.
+pub const KIND_AGENT_CONFIG_UPDATED: u32 = 47201;
+/// Skill or command imported (e.g. from GitHub).
+pub const KIND_AGENT_SKILL_IMPORTED: u32 = 47250;
+/// Session telemetry: token usage, cost, tool-call per turn.
+pub const KIND_AGENT_SESSION_TELEMETRY: u32 = 47300;
+/// Dependency graph edge (agent→command, command→skill).
+pub const KIND_AGENT_GRAPH_EDGE: u32 = 47350;
+
+// User groups (47000–47999) — Agent Studio kinds above use 47200–47399.
// System / admin custom range (48000–48999)
/// An audit log entry was recorded.
@@ -745,6 +785,23 @@ pub const ALL_KINDS: &[u32] = &[
KIND_WORKFLOW_APPROVAL_REQUESTED,
KIND_WORKFLOW_APPROVAL_GRANTED,
KIND_WORKFLOW_APPROVAL_DENIED,
+ KIND_FLOW_GRAPH_SAVED,
+ KIND_FLOW_BLOCK_EXECUTED,
+ KIND_FLOW_BLOCK_FAILED,
+ KIND_FLOW_KB_DOCUMENT_INGESTED,
+ KIND_FLOW_KB_EMBEDDING_INDEXED,
+ KIND_FLOW_KB_SEMANTIC_QUERY,
+ KIND_FLOW_TABLE_ROW_CREATED,
+ KIND_FLOW_TABLE_ROW_UPDATED,
+ KIND_FLOW_TABLE_ROW_DELETED,
+ KIND_FLOW_FILE_UPLOADED,
+ KIND_FLOW_FILE_VERSIONED,
+ KIND_FLOW_FILE_DELETED,
+ KIND_AGENT_CONFIG_CREATED,
+ KIND_AGENT_CONFIG_UPDATED,
+ KIND_AGENT_SKILL_IMPORTED,
+ KIND_AGENT_SESSION_TELEMETRY,
+ KIND_AGENT_GRAPH_EDGE,
KIND_AUDIT_ENTRY,
KIND_HUDDLE_STARTED,
KIND_HUDDLE_PARTICIPANT_JOINED,
@@ -790,6 +847,16 @@ pub const fn is_workflow_execution_kind(kind: u32) -> bool {
kind >= KIND_WORKFLOW_TRIGGERED && kind <= KIND_WORKFLOW_APPROVAL_DENIED
}
+/// Returns `true` if `kind` is a Flow Studio event (46200–46399).
+pub const fn is_flow_studio_kind(kind: u32) -> bool {
+ kind >= KIND_FLOW_GRAPH_SAVED && kind <= 46399
+}
+
+/// Returns `true` if `kind` is an Agent Studio event (47200–47399).
+pub const fn is_agent_studio_kind(kind: u32) -> bool {
+ kind >= KIND_AGENT_CONFIG_CREATED && kind <= 47399
+}
+
/// Returns `true` if `kind` is a NIP-43 relay membership admin command (9030–9032)
/// or the Buzz workspace-profile admin command (9033).
pub const fn is_relay_admin_kind(kind: u32) -> bool {
@@ -864,6 +931,13 @@ const _: () = assert!(is_parameterized_replaceable(KIND_DM_VISIBILITY)); // 3062
const _: () = assert!(is_parameterized_replaceable(KIND_PROJECT)); // 30621 ∈ 30000–39999
const _: () = assert!(is_parameterized_replaceable(KIND_THREAD_SUMMARY)); // 39005 ∈ 30000–39999
const _: () = assert!(is_parameterized_replaceable(KIND_WINDOW_BOUNDS)); // 39006 ∈ 30000–39999
+const _: () = assert!(is_flow_studio_kind(KIND_FLOW_GRAPH_SAVED));
+const _: () = assert!(is_flow_studio_kind(46399));
+const _: () = assert!(!is_flow_studio_kind(KIND_WORKFLOW_APPROVAL_DENIED));
+const _: () = assert!(is_agent_studio_kind(KIND_AGENT_CONFIG_CREATED));
+const _: () = assert!(is_agent_studio_kind(47399));
+const _: () = assert!(!is_agent_studio_kind(KIND_AUDIT_ENTRY));
+const _: () = assert!(!is_agent_studio_kind(KIND_HUDDLE_STARTED));
// Compile-time: NIP-34 parameterized replaceable kinds are in the correct range.
const _: () = assert!(
diff --git a/crates/buzz-db/src/flow_studio.rs b/crates/buzz-db/src/flow_studio.rs
new file mode 100644
index 00000000000..656e30a364b
--- /dev/null
+++ b/crates/buzz-db/src/flow_studio.rs
@@ -0,0 +1,516 @@
+//! Flow Studio read-model tables (Buzz Hive projector target).
+
+use buzz_core::tenant::CommunityId;
+use sqlx::{PgPool, Row};
+
+use crate::error::Result;
+
+/// A knowledge-base search hit (keyword match on chunk content).
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct FlowKnowledgeSearchHit {
+ /// Document identifier.
+ pub document_id: String,
+ /// Chunk index within the document.
+ pub chunk_index: i32,
+ /// Matching chunk text.
+ pub content: String,
+}
+
+/// A table row in the Flow Studio read-model.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct FlowTableRowRecord {
+ /// Row identifier within the table.
+ pub row_id: String,
+ /// Row payload as JSON.
+ pub row_json: serde_json::Value,
+}
+
+/// File metadata in the Flow Studio read-model.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct FlowFileRecord {
+ /// File identifier.
+ pub file_id: String,
+ /// Original filename.
+ pub filename: String,
+ /// Blossom media URL when uploaded.
+ pub media_url: Option,
+ /// Monotonic version counter.
+ pub version: i32,
+}
+
+/// Upsert a knowledge document row from a Flow Studio ingest event.
+pub async fn upsert_knowledge_document(
+ pool: &PgPool,
+ community_id: CommunityId,
+ knowledge_base_id: &str,
+ document_id: &str,
+ filename: &str,
+ mime_type: &str,
+) -> Result<()> {
+ sqlx::query(
+ r#"
+ INSERT INTO flow_knowledge_documents
+ (community_id, document_id, knowledge_base_id, filename, mime_type)
+ VALUES ($1, $2, $3, $4, $5)
+ ON CONFLICT (community_id, document_id) DO UPDATE SET
+ knowledge_base_id = EXCLUDED.knowledge_base_id,
+ filename = EXCLUDED.filename,
+ mime_type = EXCLUDED.mime_type,
+ ingested_at = NOW()
+ "#,
+ )
+ .bind(community_id.as_uuid())
+ .bind(document_id)
+ .bind(knowledge_base_id)
+ .bind(filename)
+ .bind(mime_type)
+ .execute(pool)
+ .await?;
+ Ok(())
+}
+
+/// Upsert a table row from a Flow Studio table event.
+pub async fn upsert_table_row(
+ pool: &PgPool,
+ community_id: CommunityId,
+ table_id: &str,
+ row_id: &str,
+ row_json: &str,
+) -> Result<()> {
+ let row_value: serde_json::Value = serde_json::from_str(row_json)?;
+ sqlx::query(
+ r#"
+ INSERT INTO flow_table_rows
+ (community_id, table_id, row_id, row_json, updated_at, deleted_at)
+ VALUES ($1, $2, $3, $4, NOW(), NULL)
+ ON CONFLICT (community_id, table_id, row_id) DO UPDATE SET
+ row_json = EXCLUDED.row_json,
+ updated_at = NOW(),
+ deleted_at = NULL
+ "#,
+ )
+ .bind(community_id.as_uuid())
+ .bind(table_id)
+ .bind(row_id)
+ .bind(row_value)
+ .execute(pool)
+ .await?;
+ Ok(())
+}
+
+/// Soft-delete a table row from a Flow Studio delete event.
+pub async fn delete_table_row(
+ pool: &PgPool,
+ community_id: CommunityId,
+ table_id: &str,
+ row_id: &str,
+) -> Result<()> {
+ sqlx::query(
+ r#"
+ UPDATE flow_table_rows
+ SET deleted_at = NOW(), updated_at = NOW()
+ WHERE community_id = $1 AND table_id = $2 AND row_id = $3
+ "#,
+ )
+ .bind(community_id.as_uuid())
+ .bind(table_id)
+ .bind(row_id)
+ .execute(pool)
+ .await?;
+ Ok(())
+}
+
+/// Index a single text chunk for keyword / vector search.
+pub async fn upsert_knowledge_embedding(
+ pool: &PgPool,
+ community_id: CommunityId,
+ document_id: &str,
+ embedding_id: &str,
+ chunk_index: i32,
+ content: &str,
+ embedding: &[f32],
+) -> Result<()> {
+ let vector_literal = format_pgvector(embedding);
+ sqlx::query(
+ r#"
+ INSERT INTO flow_knowledge_embeddings
+ (community_id, embedding_id, document_id, chunk_index, content, embedding)
+ VALUES ($1, $2, $3, $4, $5, $6::vector)
+ ON CONFLICT (community_id, embedding_id) DO UPDATE SET
+ content = EXCLUDED.content,
+ chunk_index = EXCLUDED.chunk_index,
+ embedding = EXCLUDED.embedding,
+ created_at = NOW()
+ "#,
+ )
+ .bind(community_id.as_uuid())
+ .bind(embedding_id)
+ .bind(document_id)
+ .bind(chunk_index)
+ .bind(content)
+ .bind(vector_literal)
+ .execute(pool)
+ .await?;
+ Ok(())
+}
+
+fn format_pgvector(values: &[f32]) -> String {
+ let inner: Vec = values.iter().map(|v| v.to_string()).collect();
+ format!("[{}]", inner.join(","))
+}
+
+/// Cosine-distance semantic search over indexed chunks (`<=>` operator).
+pub async fn search_knowledge_semantic(
+ pool: &PgPool,
+ community_id: CommunityId,
+ knowledge_base_id: &str,
+ query_embedding: &[f32],
+ limit: i64,
+) -> Result> {
+ let capped = limit.clamp(1, 50);
+ let vector_literal = format_pgvector(query_embedding);
+ let rows = sqlx::query(
+ r#"
+ SELECT e.document_id, e.chunk_index, e.content,
+ (e.embedding <=> $3::vector) AS distance
+ FROM flow_knowledge_embeddings e
+ INNER JOIN flow_knowledge_documents d
+ ON d.community_id = e.community_id AND d.document_id = e.document_id
+ WHERE e.community_id = $1
+ AND d.knowledge_base_id = $2
+ ORDER BY distance ASC
+ LIMIT $4
+ "#,
+ )
+ .bind(community_id.as_uuid())
+ .bind(knowledge_base_id)
+ .bind(vector_literal)
+ .bind(capped)
+ .fetch_all(pool)
+ .await?;
+
+ Ok(rows
+ .into_iter()
+ .filter_map(|row| {
+ Some(FlowKnowledgeSearchHit {
+ document_id: row.try_get("document_id").ok()?,
+ chunk_index: row.try_get("chunk_index").ok()?,
+ content: row.try_get("content").ok()?,
+ })
+ })
+ .collect())
+}
+
+/// List active rows for a table.
+pub async fn list_table_rows(
+ pool: &PgPool,
+ community_id: CommunityId,
+ table_id: &str,
+ limit: i64,
+) -> Result> {
+ let rows = sqlx::query(
+ r#"
+ SELECT row_id, row_json
+ FROM flow_table_rows
+ WHERE community_id = $1 AND table_id = $2 AND deleted_at IS NULL
+ ORDER BY updated_at DESC
+ LIMIT $3
+ "#,
+ )
+ .bind(community_id.as_uuid())
+ .bind(table_id)
+ .bind(limit.clamp(1, 200))
+ .fetch_all(pool)
+ .await?;
+
+ Ok(rows
+ .into_iter()
+ .filter_map(|row| {
+ Some(FlowTableRowRecord {
+ row_id: row.try_get("row_id").ok()?,
+ row_json: row.try_get("row_json").ok()?,
+ })
+ })
+ .collect())
+}
+
+/// Upsert file metadata from a Flow Studio file event.
+pub async fn upsert_flow_file(
+ pool: &PgPool,
+ community_id: CommunityId,
+ file_id: &str,
+ filename: &str,
+ media_url: Option<&str>,
+) -> Result<()> {
+ sqlx::query(
+ r#"
+ INSERT INTO flow_files
+ (community_id, file_id, filename, media_url, version, deleted_at, updated_at)
+ VALUES ($1, $2, $3, $4, 1, NULL, NOW())
+ ON CONFLICT (community_id, file_id) DO UPDATE SET
+ filename = EXCLUDED.filename,
+ media_url = EXCLUDED.media_url,
+ version = flow_files.version + 1,
+ deleted_at = NULL,
+ updated_at = NOW()
+ "#,
+ )
+ .bind(community_id.as_uuid())
+ .bind(file_id)
+ .bind(filename)
+ .bind(media_url)
+ .execute(pool)
+ .await?;
+ Ok(())
+}
+
+/// Soft-delete file metadata.
+pub async fn delete_flow_file(
+ pool: &PgPool,
+ community_id: CommunityId,
+ file_id: &str,
+) -> Result<()> {
+ sqlx::query(
+ r#"
+ UPDATE flow_files
+ SET deleted_at = NOW(), updated_at = NOW()
+ WHERE community_id = $1 AND file_id = $2
+ "#,
+ )
+ .bind(community_id.as_uuid())
+ .bind(file_id)
+ .execute(pool)
+ .await?;
+ Ok(())
+}
+
+/// List active files for a community.
+pub async fn list_flow_files(
+ pool: &PgPool,
+ community_id: CommunityId,
+ limit: i64,
+) -> Result> {
+ let rows = sqlx::query(
+ r#"
+ SELECT file_id, filename, media_url, version
+ FROM flow_files
+ WHERE community_id = $1 AND deleted_at IS NULL
+ ORDER BY updated_at DESC
+ LIMIT $2
+ "#,
+ )
+ .bind(community_id.as_uuid())
+ .bind(limit.clamp(1, 200))
+ .fetch_all(pool)
+ .await?;
+
+ Ok(rows
+ .into_iter()
+ .filter_map(|row| {
+ Some(FlowFileRecord {
+ file_id: row.try_get("file_id").ok()?,
+ filename: row.try_get("filename").ok()?,
+ media_url: row.try_get("media_url").ok(),
+ version: row.try_get("version").ok()?,
+ })
+ })
+ .collect())
+}
+
+/// Load the latest saved canvas graph for a flow id (kind 46200, `d` tag).
+pub async fn get_latest_flow_graph(
+ pool: &PgPool,
+ community_id: CommunityId,
+ flow_id: &str,
+) -> Result
-
- Approval actions are not yet available in Desktop.
-
+
+
+
+
);
}
diff --git a/desktop/src/features/workflows/ui/WorkflowRunTrace.tsx b/desktop/src/features/workflows/ui/WorkflowRunTrace.tsx
index 76fc9ecf886..19f575a405d 100644
--- a/desktop/src/features/workflows/ui/WorkflowRunTrace.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowRunTrace.tsx
@@ -42,6 +42,7 @@ function StepStatusIcon({ status }: { status: string }) {
case "skipped":
return ;
case "waiting_approval":
+ case "suspended":
return ;
default:
return ;
diff --git a/desktop/src/shared/api/tauriHiveStudio.ts b/desktop/src/shared/api/tauriHiveStudio.ts
new file mode 100644
index 00000000000..6c3c1a506ee
--- /dev/null
+++ b/desktop/src/shared/api/tauriHiveStudio.ts
@@ -0,0 +1,80 @@
+import { invokeTauri } from "@/shared/api/tauri";
+
+type PublishEventResponse = {
+ accepted: boolean;
+ event_id: string;
+ message: string;
+};
+
+type FlowGraphResponse = {
+ flow_id: string;
+ graph_json: string | null;
+ found: boolean;
+ event_id?: string;
+};
+
+export function publishFlowGraph(
+ flowId: string,
+ graphJson: string,
+): Promise {
+ return invokeTauri("publish_flow_graph", { flowId, graphJson });
+}
+
+export function getFlowGraph(flowId: string): Promise {
+ return invokeTauri("get_flow_graph", { flowId });
+}
+
+export function publishSkillImport(
+ skillId: string,
+ sourceRepo?: string | null,
+ sourceCommit?: string | null,
+): Promise {
+ return invokeTauri("publish_skill_import", {
+ skillId,
+ sourceRepo: sourceRepo ?? null,
+ sourceCommit: sourceCommit ?? null,
+ });
+}
+
+export function publishKbDocument(input: {
+ knowledgeBaseId: string;
+ documentId: string;
+ filename: string;
+ mimeType: string;
+ content?: string | null;
+}): Promise {
+ return invokeTauri("publish_kb_document", {
+ knowledgeBaseId: input.knowledgeBaseId,
+ documentId: input.documentId,
+ filename: input.filename,
+ mimeType: input.mimeType,
+ content: input.content ?? null,
+ });
+}
+
+export function publishTableRow(
+ tableId: string,
+ rowId: string,
+ rowJson: string,
+): Promise {
+ return invokeTauri("publish_table_row", { tableId, rowId, rowJson });
+}
+
+export function deleteTableRow(
+ tableId: string,
+ rowId: string,
+): Promise {
+ return invokeTauri("delete_table_row", { tableId, rowId });
+}
+
+export function publishFlowFile(
+ fileId: string,
+ filename: string,
+ mediaUrl?: string | null,
+): Promise {
+ return invokeTauri("publish_flow_file", {
+ fileId,
+ filename,
+ mediaUrl: mediaUrl ?? null,
+ });
+}
diff --git a/desktop/src/shared/constants/kinds.ts b/desktop/src/shared/constants/kinds.ts
index b98ba37d75c..b92bdbc4e1e 100644
--- a/desktop/src/shared/constants/kinds.ts
+++ b/desktop/src/shared/constants/kinds.ts
@@ -58,6 +58,16 @@ export const KIND_MANAGED_AGENT = 30177;
export const KIND_USER_STATUS = 30315;
export const KIND_AGENT_OBSERVER_FRAME = 24200;
export const KIND_AGENT_TURN_METRIC = 44200;
+// Buzz Hive — Flow Studio (46200–46399) and Agent Studio (47200–47399).
+// Mirror of buzz-core/src/kind.rs.
+export const KIND_FLOW_GRAPH_SAVED = 46200;
+export const KIND_FLOW_BLOCK_EXECUTED = 46201;
+export const KIND_FLOW_BLOCK_FAILED = 46202;
+export const KIND_AGENT_CONFIG_CREATED = 47200;
+export const KIND_AGENT_CONFIG_UPDATED = 47201;
+export const KIND_AGENT_SKILL_IMPORTED = 47250;
+export const KIND_AGENT_SESSION_TELEMETRY = 47300;
+export const KIND_AGENT_GRAPH_EDGE = 47350;
export const KIND_EVENT_REMINDER = 30300;
export const KIND_REPO_ANNOUNCEMENT = 30617;
export const KIND_REPO_STATE = 30618;
diff --git a/desktop/src/shared/ui/ViewLoadingFallback.tsx b/desktop/src/shared/ui/ViewLoadingFallback.tsx
index 5fe43594976..a4ae59fcd80 100644
--- a/desktop/src/shared/ui/ViewLoadingFallback.tsx
+++ b/desktop/src/shared/ui/ViewLoadingFallback.tsx
@@ -10,7 +10,9 @@ type ViewLoadingFallbackKind =
| "forum"
| "projects"
| "pulse"
- | "workflows";
+ | "workflows"
+ | "flow-studio"
+ | "agent-studio";
type ViewLoadingFallbackProps = {
includeHeader?: boolean;
@@ -402,6 +404,8 @@ export function ViewLoadingFallback({
{shouldShowChannelHeader ? : null}
{kind === "agents" ? : null}
{kind === "workflows" ? : null}
+ {kind === "flow-studio" ? : null}
+ {kind === "agent-studio" ? : null}
{kind === "projects" ? : null}
{kind === "channel" ? (
diff --git a/desktop/tests/e2e/hive-studio.spec.ts b/desktop/tests/e2e/hive-studio.spec.ts
new file mode 100644
index 00000000000..8b0327b9851
--- /dev/null
+++ b/desktop/tests/e2e/hive-studio.spec.ts
@@ -0,0 +1,69 @@
+import { expect, test } from "@playwright/test";
+
+import { installMockBridge } from "../helpers/bridge";
+
+test.beforeEach(async ({ page }) => {
+ await page.route("**/flow-studio/blocks", async (route) => {
+ await route.fulfill({
+ contentType: "application/json",
+ body: JSON.stringify({
+ blocks: [
+ {
+ block_type: "http",
+ label: "HTTP Request",
+ category: "http",
+ description: "Call an external URL",
+ },
+ ],
+ }),
+ });
+ });
+
+ await page.route("**/agent-studio/graph", async (route) => {
+ await route.fulfill({
+ contentType: "application/json",
+ body: JSON.stringify({
+ nodes: [{ id: "agent:scout", kind: "agent", slug: "scout" }],
+ edges: [],
+ }),
+ });
+ });
+
+ await page.route("**/agent-studio/costs", async (route) => {
+ await route.fulfill({
+ contentType: "application/json",
+ body: JSON.stringify({
+ total_cost_usd: 0.042,
+ acp_session_cost_usd: 0.041,
+ flow_block_cost_usd: 0.001,
+ total_tokens: 1200,
+ session_count: 1,
+ sessions: [],
+ }),
+ });
+ });
+
+ await installMockBridge(page);
+});
+
+test("navigates to Flow Studio and shows canvas shell", async ({ page }) => {
+ await page.goto("/");
+ await page.getByTestId("open-flow-studio-view").click();
+ await expect(page).toHaveURL(/#\/flow-studio$/);
+ await expect(page.getByTestId("flow-studio-view")).toBeVisible();
+ await expect(
+ page.getByRole("heading", { name: "Flow Studio" }),
+ ).toBeVisible();
+ await expect(page.getByText("HTTP Request")).toBeVisible();
+});
+
+test("navigates to Agent Studio and shows graph shell", async ({ page }) => {
+ await page.goto("/");
+ await page.getByTestId("open-agent-studio-view").click();
+ await expect(page).toHaveURL(/#\/agent-studio$/);
+ await expect(page.getByTestId("agent-studio-view")).toBeVisible();
+ await expect(
+ page.getByRole("heading", { name: "Agent Studio" }),
+ ).toBeVisible();
+ await expect(page.getByText("scout")).toBeVisible();
+});
diff --git a/docker-compose.harness.yml b/docker-compose.harness.yml
index f687a9f8428..9a2455e702f 100644
--- a/docker-compose.harness.yml
+++ b/docker-compose.harness.yml
@@ -13,7 +13,7 @@
# =============================================================================
services:
postgres:
- image: postgres:17-alpine
+ image: pgvector/pgvector:pg17
environment:
POSTGRES_USER: buzz
POSTGRES_PASSWORD: buzz_dev
diff --git a/docker-compose.yml b/docker-compose.yml
index e7dc09fafc4..daa953d235d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,7 +2,7 @@ name: buzz
services:
postgres:
- image: postgres:17-alpine
+ image: pgvector/pgvector:pg17
container_name: buzz-postgres
environment:
POSTGRES_USER: buzz
diff --git a/docs/BUZZ_HIVE_IMPLEMENTATION_PLAN.md b/docs/BUZZ_HIVE_IMPLEMENTATION_PLAN.md
new file mode 100644
index 00000000000..0abdbf69748
--- /dev/null
+++ b/docs/BUZZ_HIVE_IMPLEMENTATION_PLAN.md
@@ -0,0 +1,225 @@
+# Implementation Plan: **Buzz Hive**
+
+> Đi kèm với `BUZZ_HIVE_MERGE_SPEC.md`. Tài liệu này trả lời **ai làm gì, khi nào, xong khi nào biết là xong**.
+> Ước lượng dựa trên team giả định: 2 Rust backend, 2 frontend (React/Tauri), 1 DevOps/infra kiêm bán thời gian, 1 PM/tech lead.
+
+---
+
+## 0. Nguyên tắc lập kế hoạch
+
+- Đi theo đúng 5 giai đoạn P0–P5 trong spec, nhưng mỗi giai đoạn ở đây được chẻ thành **sprint 2 tuần** với task cụ thể + Definition of Done (DoD).
+- **Không giai đoạn nào bắt đầu khi giai đoạn trước chưa qua DoD** — trừ P1 và một phần P2 có thể chạy song song vì ít phụ thuộc lẫn nhau (Agent Studio ít đụng workflow engine hơn Flow Studio).
+- Mỗi sprint kết thúc bằng **demo chạy thật** trên nhánh `main`, không demo bằng slide.
+
+---
+
+## 1. Tổng quan timeline (ước lượng ~24 tuần / 6 tháng, 12 sprint)
+
+| Giai đoạn | Sprint | Tuần | Nội dung chính |
+|---|---|---|---|
+| P0 — Khung sườn | S1–S2 | 1–4 | Fork, dựng skeleton crate, định nghĩa kind, CI |
+| P1 — Agent Studio | S3–S4 | 5–8 | Port graph/skill-import, event kind 472xx/473xx |
+| P2 — Flow Studio (lõi) | S5–S7 | 9–14 | Block/tool registry, canvas React, nối workflow engine |
+| P3 — Knowledge/Tables/Files | S8–S9 | 15–18 | pgvector, projector event→Postgres, UI panel |
+| P4 — Hợp nhất Session Monitor | S10 | 19–20 | 1 màn hình cost/token chung |
+| P5 — Dọn dẹp & Rebrand | S11–S12 | 21–24 | Xoá code thừa, hardening, release bản đầu |
+
+Có thể rút còn ~16 tuần nếu bỏ song song hoá an toàn và chấp nhận rủi ro kỹ thuật cao hơn ở P2.
+
+---
+
+## 2. Chi tiết theo Sprint
+
+### 🟦 Sprint 1 (Tuần 1–2) — Khởi tạo repo & workspace
+
+**Mục tiêu:** có `buzz-hive` build xanh, chưa cần chạy được gì mới.
+
+| Task | Người phụ trách | Output |
+|---|---|---|
+| Fork `block/buzz` → `buzz-hive`, giữ nguyên lịch sử git, cập nhật `README`/`AGENTS.md` | Tech lead | Repo mới, CI pass |
+| Audit schema Postgres hiện tại của Buzz (`buzz-db`) — xác nhận có/không pgvector, đối chiếu với schema Drizzle của Sim | Backend #1 | `docs/DB_AUDIT.md` |
+| Đọc source `buzz-workflow` để hiểu rõ `StepResult`, `Suspended`, approval-gate hiện tại (đang 🚧 WF-08) | Backend #2 | Ghi chú kỹ thuật + issue fix WF-08 |
+| Xác nhận license `claude-code-cli-ui` (MIT) tương thích Apache-2.0, chuẩn bị NOTICE file | Tech lead | `NOTICE` cập nhật |
+| Setup Hermit env + `just setup` chạy được trên máy cả team | DevOps | Onboarding doc |
+
+**DoD:** `just ci` xanh trên fork, doc audit DB xong, issue WF-08 được tạo và ước lượng.
+
+---
+
+### 🟦 Sprint 2 (Tuần 3–4) — Skeleton crate mới + Kind registry
+
+| Task | Người phụ trách | Output |
+|---|---|---|
+| Tạo `crates/buzz-flow` rỗng (chỉ `lib.rs`, `Cargo.toml`, đăng ký vào workspace) | Backend #1 | Crate compile, chưa có logic |
+| Tạo `crates/buzz-agent-studio` rỗng tương tự | Backend #2 | Crate compile |
+| Viết `events.rs` cho cả 2 crate — định nghĩa struct Rust cho toàn bộ kind 46200–46399 và 47200–47399 theo bảng ở spec mục 4 | Backend #1 + #2 | Kind định nghĩa, unit test serialize/deserialize NIP-01 |
+| Thêm feature flag / route rỗng cho `flow-studio` và `agent-studio` trong `desktop/src/features/` (chỉ khung React, chưa logic) | Frontend #1 | Tab hiện trong sidebar, nội dung "Coming soon" |
+| CI: thêm job build cho 2 crate mới | DevOps | Pipeline pass |
+
+**DoD:** Build workspace pass với 2 crate mới; toàn bộ kind number có test roundtrip; 2 tab UI rỗng hiển thị được trong desktop app.
+
+---
+
+### 🟩 Sprint 3 (Tuần 5–6) — Agent Studio: Dependency graph
+
+| Task | Người phụ trách |
+|---|---|
+| Port thuật toán scan frontmatter (agent/command/skill → tham chiếu `agent:` / `/command`) từ `claude-code-cli-ui` sang Rust trong `graph.rs` | Backend #2 |
+| Thiết kế API: `GET /agent-studio/graph` trả về node/edge, backed bởi event kind 47350–47399 | Backend #2 |
+| Port UI `AgentGraph.tsx` (Vue → React), dùng lib graph tương đương (vd. reactflow) thay VueFlow | Frontend #1 |
+| Viết event `agent_config_created` (47200) khi user tạo/sửa persona qua Agent Studio, nối vào `buzz-persona` hiện có | Backend #1 |
+
+**DoD:** Tạo 1 persona mới qua UI → thấy node xuất hiện trên graph, event log thấy đúng kind 47200 + 47350.
+
+---
+
+### 🟩 Sprint 4 (Tuần 7–8) — Agent Studio: GitHub import + Session monitor (v1)
+
+| Task | Người phụ trách |
+|---|---|
+| Port `skill_import.rs`: nhập skill từ GitHub repo (clone/tải, parse, ghi thành event kind 47250) | Backend #2 |
+| UI `SkillImportModal.tsx`: nhập URL repo → chọn skill → import | Frontend #1 |
+| Port `monitor.rs` v1: nhận stream token/cost/tool-call từ ACP session (`buzz-acp`), phát event kind 47300 | Backend #1 |
+| UI `SessionMonitor.tsx` v1: bảng real-time qua SSE | Frontend #2 |
+| **Demo P1 hoàn chỉnh** trước stakeholder | Cả team |
+
+**DoD (chốt P1):** Import 1 skill thật từ GitHub công khai → skill khả dụng trong danh sách persona; theo dõi 1 phiên Claude Code chạy qua `buzz-acp` thấy token/cost cập nhật real-time trên UI.
+
+---
+
+### 🟨 Sprint 5 (Tuần 9–10) — Flow Studio: Block/Tool registry
+
+| Task | Người phụ trách |
+|---|---|
+| Port cấu trúc block/tool registry của Sim sang Rust (`blocks/`, `tools/`) — tối thiểu: Agent block, Condition block, HTTP block, Code block | Backend #1 |
+| Định nghĩa cách 1 block map sang `StepResult` của `buzz-workflow` hiện có (không viết engine mới) | Backend #1 + #2 |
+| Viết fix cho WF-08 (persist approval token, resume `execute_from_step`) — **bắt buộc xong trước khi có block "Human approval"** | Backend #2 |
+| Thiết kế event kind 46200–46249 (flow saved / block executed / block failed) | Backend #1 |
+
+**DoD:** Chạy được 1 workflow gồm 2 block (HTTP → Condition) hoàn toàn qua CLI/test, không cần UI, log đúng event, WF-08 fixed và có test.
+
+---
+
+### 🟨 Sprint 6 (Tuần 11–12) — Flow Studio: Canvas UI
+
+| Task | Người phụ trách |
+|---|---|
+| `Canvas.tsx` kéo-thả (reactflow), tương tác với block registry qua API | Frontend #1 + #2 |
+| `BlockPalette.tsx` — danh sách block khả dụng, kéo vào canvas | Frontend #2 |
+| Nút "Chuyển đổi YAML ↔ Canvas" cho workflow cũ của Buzz (tương thích ngược) | Frontend #1 + Backend #2 |
+| Nối persona/agent (từ Agent Studio) làm 1 loại block trong palette | Backend #1 + Frontend #1 |
+
+**DoD:** Người dùng tạo workflow bằng kéo-thả từ đầu đến cuối, lưu, chạy, thấy kết quả trong channel Buzz — không cần sửa YAML tay.
+
+---
+
+### 🟨 Sprint 7 (Tuần 13–14) — Flow Studio: hoàn thiện + Human approval block
+
+| Task | Người phụ trách |
+|---|---|
+| Block "Human approval" dùng approval-gate đã fix ở Sprint 5 | Backend #2 |
+| Loop block, error-handling/retry ở cấp block | Backend #1 |
+| UI: trạng thái block (running/success/fail/suspended) hiển thị trực tiếp trên canvas | Frontend #1 |
+| Test tải: 100 workflow chạy đồng thời qua `Arc` (đã có), xác nhận `CapacityExceeded` trả đúng, không deadlock | Backend #2 + DevOps |
+| **Demo chốt P2** | Cả team |
+
+**DoD (chốt P2):** Một workflow có bước cần người duyệt → tạm dừng đúng, người dùng duyệt trong Buzz UI (không phải Flow Studio riêng) → workflow resume và chạy tiếp.
+
+---
+
+### 🟧 Sprint 8 (Tuần 15–16) — Knowledge base + Projector event→Postgres
+
+| Task | Người phụ trách |
+|---|---|
+| Thêm pgvector extension vào `buzz-db` (nếu Sprint 1 audit xác nhận chưa có) | Backend #1 + DevOps |
+| Viết "projector": subscribe relay, nhận event kind 46250–46299 → ghi/update row Postgres (đọc nhanh, KHÔNG phải nguồn sự thật) | Backend #2 |
+| `knowledge/` crate: ingest document → embedding → lưu vector, expose semantic search API | Backend #1 |
+| UI `KnowledgeBasePanel.tsx`: upload tài liệu, tìm kiếm ngữ nghĩa | Frontend #2 |
+
+**DoD:** Upload 1 file text → thấy event ingest → query ngữ nghĩa trả kết quả đúng trong <2s.
+
+---
+
+### 🟧 Sprint 9 (Tuần 17–18) — Tables + Files
+
+| Task | Người phụ trách |
+|---|---|
+| `tables.rs`: CRUD row qua event kind 46300–46349, projector tương ứng | Backend #2 |
+| `files.rs`: upload/version/xoá qua event kind 46350–46399, tái sử dụng cơ chế media của Buzz (media sharing đã có) thay vì viết storage riêng | Backend #1 |
+| UI `TablesPanel.tsx`, `FilesPanel.tsx` | Frontend #1 + #2 |
+| Đảm bảo toàn bộ Tables/Files/Knowledge scope đúng theo `community_id` (multi-tenant) — viết test isolation | Backend #2 |
+
+**DoD (chốt P3):** Test 2 community khác nhau không thấy dữ liệu Tables/Files/Knowledge của nhau (test tự động, không chỉ kiểm tra tay).
+
+---
+
+### 🟥 Sprint 10 (Tuần 19–20) — Hợp nhất Session Monitor
+
+| Task | Người phụ trách |
+|---|---|
+| Mở rộng `monitor.rs` để nhận cả token/cost từ block "Agent" trong Flow Studio, không chỉ session ACP của Agent Studio | Backend #1 |
+| 1 màn hình `UnifiedCostMonitor.tsx` gộp cả 2 nguồn | Frontend #1 |
+| Cảnh báo ngưỡng chi phí (theo community), thông báo qua channel Buzz | Backend #2 + Frontend #2 |
+
+**DoD (chốt P4):** Chạy 1 workflow gọi agent + 1 phiên Claude Code trực tiếp, cả 2 chi phí cộng dồn đúng trên 1 dashboard.
+
+---
+
+### ⬛ Sprint 11 (Tuần 21–22) — Dọn dẹp
+
+| Task | Người phụ trách |
+|---|---|
+| Xoá toàn bộ code Next.js (Sim) / Nuxt (cli-ui) còn sót trong repo tạm dùng để tham chiếu port | Frontend #1 + #2 |
+| Chuẩn hoá style/lint theo chuẩn Buzz (Rust fmt, biome cho desktop/web/mobile) | DevOps |
+| Cập nhật `ARCHITECTURE.md`, `VISION.md`, `AGENTS.md` phản ánh kiến trúc mới | Tech lead |
+| Security review: auth path của Flow Studio & Agent Studio không có đường vòng qua auth Buzz | Backend #1 + #2 |
+
+**DoD:** Không còn file `.tsx`/`.vue` gốc ngoài source đã port; docs khớp code thật; security review ký duyệt.
+
+---
+
+### ⬛ Sprint 12 (Tuần 23–24) — Hardening & Release
+
+| Task | Người phụ trách |
+|---|---|
+| Load test toàn hệ thống (chat + workflow + agent studio đồng thời) | DevOps |
+| Viết `RELEASING.md` cập nhật cho luồng release mới (đã có sẵn ở Buzz, chỉ bổ sung 2 module) | Tech lead |
+| Beta release nội bộ, thu thập feedback 1 tuần | Cả team |
+| Fix bug chặn release, tag `v0.1.0-buzz-hive` | Cả team |
+
+**DoD (chốt toàn dự án):** Bản release `v0.1.0-buzz-hive` chạy self-host qua Docker Compose, đủ 3 tính năng: Channel/Workflow gốc, Flow Studio, Agent Studio.
+
+---
+
+## 3. Bảng phân bổ nhân sự (RACI rút gọn)
+
+| Vai trò | P0 | P1 | P2 | P3 | P4 | P5 |
+|---|---|---|---|---|---|---|
+| Tech Lead | R | C | C | C | C | R |
+| Backend #1 | R | C | R | R | R | C |
+| Backend #2 | C | R | R | C | C | C |
+| Frontend #1 | C | R | R | C | C | R |
+| Frontend #2 | — | C | C | R | R | R |
+| DevOps | R | C | C | R | — | R |
+
+(R = Responsible/thực thi chính, C = Contribute/hỗ trợ)
+
+---
+
+## 4. Rủi ro theo lịch & phương án dự phòng
+
+| Rủi ro | Ảnh hưởng lịch | Phương án |
+|---|---|---|
+| WF-08 (approval gate) phức tạp hơn ước lượng | Trễ Sprint 5–7 (P2) | Cắt block "Human approval" ra khỏi P2, đẩy sang P4, Flow Studio v1 ship không có approval |
+| pgvector chưa có sẵn trong `buzz-db`, cần migrate dữ liệu lớn | Trễ Sprint 8 | Thêm 1 sprint đệm P3, chạy migration nền trong lúc P2 đang chạy |
+| Port UI React từ 2 framework khác quá tốn công | Trễ tất cả sprint có Frontend | Ưu tiên port logic (hooks/state) trước, UI thô (chưa đẹp) trước, polish ở P5 |
+| License MIT/Apache xung đột phát sinh khi audit kỹ | Chặn toàn bộ port code cli-ui | Viết lại thuật toán graph/skill-import từ đặc tả hành vi thay vì copy code, chỉ giữ ý tưởng |
+
+---
+
+## 5. Chỉ số thành công (Success Metrics) sau P5
+
+- 100% dữ liệu Flow Studio/Agent Studio đi qua event log (audit bằng cách tắt projector, xác nhận event vẫn tái tạo được toàn bộ state).
+- Độ trễ workflow event → UI cập nhật < 500ms (kế thừa yêu cầu real-time của Buzz gốc).
+- 0 identity song song ngoài Nostr keypair cho agent.
+- Community A không truy cập được bất kỳ dữ liệu Flow/Agent Studio nào của Community B (test tự động trong CI, không phải kiểm tra thủ công).
diff --git a/docs/BUZZ_HIVE_MERGE_SPEC.md b/docs/BUZZ_HIVE_MERGE_SPEC.md
new file mode 100644
index 00000000000..2620bd82ac6
--- /dev/null
+++ b/docs/BUZZ_HIVE_MERGE_SPEC.md
@@ -0,0 +1,225 @@
+# File Spec: **Buzz Hive** — Hợp nhất Buzz + claude-code-cli-ui + Sim
+
+> Trạng thái: Draft v0.1 · Ngày: 2026-08-17
+> Nhân vật chính (core platform): **`block/buzz`**
+> Sáp nhập vào: **`Ngxba/claude-code-cli-ui`** (Agent Studio) + **`simstudioai/sim`** (Flow/Knowledge Studio)
+
+---
+
+## 1. Tóm tắt & Nguyên tắc thiết kế
+
+Ý tưởng: **Buzz** đã là một "hive mind communication platform" — mọi hành động (chat, reaction, workflow step, git event) là một **Nostr event đã ký** trong một event log duy nhất, có community/tenant làm ranh giới. Đây chính là "xương sống" phù hợp nhất để làm lõi hệ thống, vì nó vốn được thiết kế cho việc người + AI agent cùng làm việc trong một không gian.
+
+Hai dự án còn lại **không được giữ làm sản phẩm độc lập** — chúng bị "mổ xẻ" và cấy vào Buzz như hai module/crate mới:
+
+| Dự án gốc | Vai trò sau khi hợp nhất | Vị trí trong Buzz |
+|---|---|---|
+| `block/buzz` | **Lõi (core)**: relay, auth, pubsub, search, audit, workflow engine, desktop/web/mobile client, agent surface (ACP) | Toàn bộ `apps/`, `crates/` gốc — giữ nguyên |
+| `simstudioai/sim` | **Buzz Flow Studio**: visual workflow builder, block/tool registry, knowledge base (pgvector), Tables, Files, Chat | Module mới `buzz-flow` (backend) + `desktop/src/features/flow-studio` (frontend) |
+| `Ngxba/claude-code-cli-ui` | **Buzz Agent Studio**: orchestration/giám sát agent, quản lý skill/command/agent, dependency graph, GitHub import skill | Module mới `buzz-agent-studio` (backend) + `desktop/src/features/agent-studio` (frontend) |
+
+Nguyên tắc bắt buộc khi merge:
+
+1. **Một event log duy nhất.** Sim và cli-ui hiện có DB/state riêng (Postgres+Drizzle cho Sim, file `.claude/*` cho cli-ui). Sau khi merge, **không app nào được ghi state ngoài Nostr event** — mọi write phải đi qua `buzz-relay` dưới dạng event có `kind` mới, để giữ tính năng "audit trail" và "portable identity" của Buzz.
+2. **Buzz community = tenant boundary** cho cả 3 tính năng — Flow Studio và Agent Studio đều bị scope theo `community` (không có global workspace ẩn).
+3. **Không tạo LLM provider riêng.** Cả 3 dự án gốc đều là "companion", không tự có model — giữ nguyên triết lý này: Buzz Hive chỉ **điều phối**, dùng key/agent runtime do người dùng cấu hình (Anthropic API key, Claude Agent SDK, Claude Code CLI, Ollama/vLLM như Sim hỗ trợ).
+4. **Persona = Agent.** `buzz-persona` (đã có trong Buzz) trở thành điểm hợp nhất khái niệm "Agent" của cli-ui và "Agent block" của Sim — tránh 2 khái niệm Agent song song.
+
+---
+
+## 2. Kiến trúc tổng thể
+
+```
+ ┌───────────────────────────────────────────┐
+ │ buzz-relay (Rust/Axum) │
+ │ Nostr WS + REST · nguồn sự thật duy nhất │
+ └───────────────┬─────────────────────────┬──┘
+ ┌──────────────────────────┼─────────────────────────┼──────────────────────┐
+ │ │ │ │
+ ┌───────▼───────┐ ┌───────▼────────┐ ┌───────▼────────┐ ┌────────▼───────┐
+ │ buzz-core / │ │ buzz-db / │ │ buzz-pubsub / │ │ buzz-audit / │
+ │ buzz-auth │ │ buzz-search │ │ presence,typing│ │ hash-chain log │
+ └────────────────┘ └─────────────────┘ └─────────────────┘ └────────────────┘
+ │
+ ┌──────────────────┴───────────────────────────────────────────────────────────┐
+ │ Tầng module nghiệp vụ (crate mới) │
+ │ │
+ │ ┌─────────────────────────────┐ ┌────────────────────────────────┐ │
+ │ │ buzz-flow (từ Sim) │ │ buzz-agent-studio (từ cli-ui) │ │
+ │ │ - workflow visual graph │ │ - agent/skill/command graph │ │
+ │ │ - block & tool registry │ │ - GitHub skill import │ │
+ │ │ - knowledge base (pgvector) │ │ - context/token/cost monitor │ │
+ │ │ - Tables, Files, Chat block │ │ - SSE session viewer │ │
+ │ │ → emits kind 462xx / 463xx │ │ → emits kind 472xx / 473xx │ │
+ │ └───────────────┬─────────────┘ └───────────────┬────────────────┘ │
+ │ │ │ │
+ │ └───────────────┬───────────────────────┘ │
+ │ ▼ │
+ │ ┌─────────────────────────┐ │
+ │ │ buzz-workflow (đã có) │ ← engine thực thi step │
+ │ │ cron, approval gate, WF │ │
+ │ └─────────────────────────┘ │
+ └───────────────────────────────────────────────────────────────────────────────┘
+ │
+ ┌──────────────────┴───────────────────────────────────────────────────────────┐
+ │ Agent surface (đã có + mở rộng) │
+ │ buzz-cli · buzz-acp (Goose/Codex/Claude Code) · buzz-agent · buzz-dev-mcp │
+ │ buzz-persona ← hợp nhất "persona pack" (Buzz) + "agent config" (cli-ui) │
+ │ + "agent block" (Sim) │
+ └───────────────────────────────────────────────────────────────────────────────┘
+ │
+ ┌──────────────────┴───────────────────────────────────────────────────────────┐
+ │ Client layer (Tauri 2 + React 19, web, mobile) │
+ │ Channels/Threads/DM/Voice (Buzz gốc) │
+ │ + Tab "Flow Studio" (canvas kéo-thả từ Sim) │
+ │ + Tab "Agent Studio" (dependency graph, monitor từ cli-ui) │
+ └─────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 3. Cấu trúc thư mục monorepo sau khi hợp nhất
+
+```
+buzz-hive/ # tên monorepo mới (root = fork của block/buzz)
+├── AGENTS.md
+├── ARCHITECTURE.md
+├── VISION.md
+├── Cargo.toml # workspace root (Rust)
+├── package.json # workspace root (bun/pnpm, TS)
+│
+├── crates/ # === Rust workspace (gốc Buzz) ===
+│ ├── buzz-core/
+│ ├── buzz-relay/
+│ ├── buzz-db/
+│ ├── buzz-auth/
+│ ├── buzz-pubsub/
+│ ├── buzz-search/
+│ ├── buzz-audit/
+│ ├── buzz-workflow/ # engine thực thi (đã có, tái sử dụng)
+│ ├── buzz-persona/ # MỞ RỘNG: hợp nhất Agent config (cli-ui) + Agent block (Sim)
+│ ├── buzz-cli/
+│ ├── buzz-acp/
+│ ├── buzz-agent/
+│ ├── buzz-dev-mcp/
+│ │
+│ ├── buzz-flow/ # MỚI — port nghiệp vụ từ simstudioai/sim
+│ │ ├── src/
+│ │ │ ├── blocks/ # registry block: agent, condition, http, code, loop...
+│ │ │ ├── tools/ # tool registry (ported từ Sim's block/tool system)
+│ │ │ ├── knowledge/ # pgvector embeddings, semantic search
+│ │ │ ├── tables.rs # "Tables" feature của Sim
+│ │ │ ├── files.rs # "Files" feature của Sim
+│ │ │ ├── chat_bridge.rs # cầu nối Sim "Chat" module ↔ Buzz channel/thread event
+│ │ │ ├── events.rs # định nghĩa kind 46200–46399 (xem mục 4)
+│ │ │ └── lib.rs
+│ │ └── Cargo.toml
+│ │
+│ └── buzz-agent-studio/ # MỚI — port nghiệp vụ từ Ngxba/claude-code-cli-ui
+│ ├── src/
+│ │ ├── graph.rs # scan agent/command/skill frontmatter → dependency graph
+│ │ ├── skill_import.rs # GitHub import flow (repo → skill)
+│ │ ├── monitor.rs # token/cost/tool-call tracking theo session
+│ │ ├── sse.rs # stream trạng thái phiên real-time
+│ │ ├── events.rs # định nghĩa kind 47200–47399 (xem mục 4)
+│ │ └── lib.rs
+│ └── Cargo.toml
+│
+├── desktop/ # Tauri 2 + React 19 (gốc Buzz)
+│ └── src/
+│ ├── features/
+│ │ ├── channels/ # Buzz gốc
+│ │ ├── workflows/ # Buzz gốc (YAML automation UI)
+│ │ ├── flow-studio/ # MỚI — canvas kéo-thả (port UI từ Sim, Next.js → React thuần)
+│ │ │ ├── Canvas.tsx
+│ │ │ ├── BlockPalette.tsx
+│ │ │ ├── KnowledgeBasePanel.tsx
+│ │ │ ├── TablesPanel.tsx
+│ │ │ └── FilesPanel.tsx
+│ │ └── agent-studio/ # MỚI — port UI từ claude-code-cli-ui (Nuxt/Vue → React)
+│ │ ├── AgentGraph.tsx # visual relationship mapping (agent/command/skill)
+│ │ ├── SessionMonitor.tsx # context/token/cost real-time (SSE)
+│ │ ├── SkillImportModal.tsx# GitHub import flow
+│ │ └── PersonaEditor.tsx
+│ └── ...
+│
+├── web/ # Buzz web client (repo browser tại myproject.com)
+├── mobile/ # Buzz mobile (Flutter, gốc)
+│
+├── migrations/ # migration duy nhất: gộp schema Postgres của Sim vào buzz-db
+│ └── 00xx_add_flow_and_agent_studio_tables.sql
+│
+└── docs/
+ └── MERGE_NOTES.md # nhật ký quyết định merge, mapping tính năng
+```
+
+---
+
+## 4. Data model — mở rộng Nostr kind
+
+Buzz mở rộng NIP-01 bằng custom `kind` integer cho từng tính năng mới, tính năng mới **không** được phá vỡ client cũ (nguyên tắc "Zero breaking changes" của Buzz).
+
+| Kind range | Nguồn gốc | Ý nghĩa |
+|---|---|---|
+| 1–9, 40001–4000x | Buzz gốc | message, reaction, channel, DM... |
+| 46001–46012 | Buzz gốc | workflow execution (đã có) |
+| **46200–46249** | **Sim → buzz-flow** | flow graph saved / block executed / block failed |
+| **46250–46299** | **Sim → buzz-flow** | knowledge base: document ingested / embedding indexed / semantic query |
+| **46300–46349** | **Sim → buzz-flow** | Tables: row created/updated/deleted (thay Drizzle+Postgres app-side state) |
+| **46350–46399** | **Sim → buzz-flow** | Files: upload / version / delete |
+| **47200–47249** | **cli-ui → buzz-agent-studio** | agent config created/updated (persona binding) |
+| **47250–47299** | **cli-ui → buzz-agent-studio** | skill/command imported (kèm nguồn GitHub repo, commit sha) |
+| **47300–47349** | **cli-ui → buzz-agent-studio** | session telemetry: token usage, cost, tool-call, per turn |
+| **47350–47399** | **cli-ui → buzz-agent-studio** | dependency-graph edge (agent→command, command→skill) |
+
+> Ghi chú kỹ thuật: các bảng nội bộ mà Sim dùng Drizzle/Postgres (Tables, Files, Knowledge) **vẫn giữ Postgres+pgvector làm read-model / cache**, nhưng **write path bắt buộc qua event** — `buzz-flow` subscribe chính relay của nó rồi project event thành row Postgres (giống cách buzz-search index Postgres FTS từ event log). Điều này giữ đúng nguyên tắc "audit trail" toàn cục của Buzz.
+
+---
+
+## 5. Điểm tích hợp UI (client)
+
+| Khu vực Buzz Desktop hiện có | Bổ sung |
+|---|---|
+| Sidebar: Channels / Threads / DMs | + mục **Flow Studio** (canvas, giống Sim) |
+| Sidebar: Workflows (YAML automation) | Nâng cấp: nút "Mở bằng Flow Studio" → chuyển YAML ↔ canvas kéo-thả |
+| Team / Persona management (đã có) | + tab **Agent Studio**: dependency graph, GitHub skill import, session monitor (từ cli-ui) |
+| Repo browser (git-sign-nostr) | Flow Studio's "Code block" và Agent Studio's "skill file" đều trỏ về cùng repo pointer đã có trong Buzz |
+
+Luồng người dùng mẫu:
+1. Người dùng tạo **Persona** "Reviewer" trong Buzz (đã có) → gán skill import từ GitHub qua **Agent Studio** (mới) → skill này xuất hiện dạng block trong **Flow Studio** (mới) để kéo vào workflow → workflow chạy qua `buzz-workflow` (đã có) → kết quả log thành event, hiện trực tiếp trong channel Buzz mà team đang xem.
+
+---
+
+## 6. Auth, quota & giới hạn kỹ thuật (kế thừa từ ARCHITECTURE.md của Buzz)
+
+- Auth: NIP-42/98 Schnorr, dùng chung cho Flow Studio API và Agent Studio API — **không tạo hệ auth song song** (Sim vốn dùng Better Auth, cli-ui dùng key cục bộ — cả hai bị loại bỏ, thay bằng Buzz auth).
+- Concurrency: workflow block execution trong `buzz-flow` tái sử dụng `Arc` pattern đã có trong `buzz-workflow` (100 permits, `try_acquire`, trả `CapacityExceeded` ngay thay vì queue).
+- Approval gate: nếu một block Sim (vd. "Human approval" block) cần dừng chờ người duyệt, dùng lại cơ chế `request_approval` / `StepResult::Suspended` đã có — **hiện đang gắn cờ 🚧 (WF-08, chưa persist token)** trong Buzz gốc, cần fix trước khi ship tính năng approval của Flow Studio.
+- Multi-tenant: mọi bảng Tables/Files/Knowledge của Sim phải được scope theo `community_id` giống cách Buzz scope cache key, search doc, audit chain hiện nay.
+
+---
+
+## 7. Lộ trình triển khai (phased)
+
+| Giai đoạn | Nội dung | Output |
+|---|---|---|
+| **P0 — Khảo sát & khung sườn** | Fork `block/buzz`, tạo skeleton `crates/buzz-flow` và `crates/buzz-agent-studio` rỗng, định nghĩa đầy đủ kind number ở mục 4 | Repo `buzz-hive` build được, chưa có tính năng |
+| **P1 — Agent Studio (dễ hơn)** | Port `graph.rs` (scan frontmatter agent/command/skill), `skill_import.rs`, gắn vào `buzz-persona` | Tab Agent Studio hoạt động, đọc/ghi qua event kind 472xx/473xx |
+| **P2 — Flow Studio (lõi)** | Port block/tool registry của Sim, canvas React, nối với `buzz-workflow` engine hiện có | Kéo-thả workflow, chạy qua engine Buzz, log event kind 46200+ |
+| **P3 — Knowledge/Tables/Files** | Port pgvector knowledge base, Tables, Files; xây projector event→Postgres | Semantic search, bảng dữ liệu, quản lý file trong workspace |
+| **P4 — Hợp nhất session monitor** | SSE token/cost monitor (cli-ui) áp dụng luôn cho session Flow Studio, không chỉ Agent Studio | 1 màn hình giám sát chi phí duy nhất cho cả 2 module |
+| **P5 — Dọn dẹp & rebrand** | Xóa mã nguồn Next.js/Nuxt còn sót không dùng, chuẩn hoá theo Tauri/React của Buzz, cập nhật `AGENTS.md`, `VISION.md` | Sản phẩm hợp nhất "Buzz Hive" phát hành bản đầu |
+
+---
+
+## 8. Rủi ro & câu hỏi mở
+
+- **Xung đột framework frontend:** Sim dùng Next.js, cli-ui dùng Nuxt/Vue, Buzz dùng React 19 + Tauri. Toàn bộ UI của 2 module mới cần **viết lại bằng React**, không thể "mount" trực tiếp — đây là phần tốn công nhất trong roadmap.
+- **Approval-gate chưa hoàn thiện (WF-08)** ở Buzz gốc — cần fix trước khi Flow Studio phụ thuộc vào nó cho các block "cần duyệt".
+- **pgvector cho knowledge base** cần thêm vào `buzz-db` (Buzz hiện dùng Postgres nhưng README không xác nhận sẵn pgvector) — cần audit schema hiện tại.
+- **Giấy phép:** Buzz và Sim đều Apache-2.0; cần xác nhận giấy phép cụ thể của `claude-code-cli-ui` (ghi MIT theo mô tả tìm được) trước khi port code — MIT vào dự án Apache-2.0 nhìn chung tương thích nhưng cần giữ đúng attribution/NOTICE.
+- **Định danh Agent xuyên hệ thống:** Buzz nhấn mạnh "danh tính agent di động, xác minh được" qua Nostr keys — cần đảm bảo Agent Studio (vốn quản lý config cục bộ trong `.claude/*`) không tạo ra một identity song song ngoài keypair Nostr của Buzz.
+
+---
+
+*Tài liệu này là bản đặc tả khái niệm dựa trên README/ARCHITECTURE/VISION công khai của 3 repo tại thời điểm 2026-08-17. Trước khi code, nên đọc trực tiếp mã nguồn `buzz-workflow`, schema Drizzle của `sim`, và cấu trúc `.claude/*` mà `claude-code-cli-ui` thao tác để chốt chi tiết field-level.*
diff --git a/docs/DB_AUDIT.md b/docs/DB_AUDIT.md
new file mode 100644
index 00000000000..8b9ee756480
--- /dev/null
+++ b/docs/DB_AUDIT.md
@@ -0,0 +1,42 @@
+# Buzz Hive — Postgres / pgvector audit (P0)
+
+> Date: 2026-08-17 · Branch: `feat/buzz-hive-p0`
+
+## Buzz (`buzz-db`) today
+
+| Capability | Status | Notes |
+|---|---|---|
+| Postgres 15+ | ✅ | Primary store via `sqlx` |
+| Full-text search | ✅ | `tsvector` + GIN (`buzz-search`) |
+| pgvector | ✅ | `CREATE EXTENSION IF NOT EXISTS vector` in `0032_buzz_hive_studio.sql` |
+| Workflow tables | ✅ | `workflows`, `workflow_runs`, `workflow_approvals` |
+| Media / Blossom | ✅ | `buzz-media` + relay HTTP |
+| Multi-tenant | ✅ | `community_id` on scoped tables |
+
+Buzz chat search uses Postgres FTS (NIP-50), not embeddings. Flow Studio knowledge uses a **read-model** with optional pgvector chunks.
+
+## Sim ([simstudioai/sim](https://github.com/simstudioai/sim)) reference
+
+Sim uses Drizzle migrations with workspace-scoped Postgres as the primary write path. Buzz Hive inverts this: **Nostr events are source of truth**; Postgres is a projector read-model (see `docs/BUZZ_HIVE_MERGE_SPEC.md` §4).
+
+## Buzz Hive read-model (migration 0032)
+
+| Feature | Table(s) | Event kinds |
+|---|---|---|
+| Knowledge docs | `flow_knowledge_documents` | 46250 |
+| Knowledge chunks | `flow_knowledge_embeddings` | 46250 (+ content in payload, MVP) |
+| Tables | `flow_table_rows` | 46300–46302 |
+| Files metadata | `flow_files` | 46350–46352 (bytes via Blossom) |
+
+Projector: `buzz-flow/src/projector.rs` → applied in `buzz-relay` on ingest.
+
+## MVP limitations
+
+- Embeddings use a deterministic hash vector (`buzz-flow/src/knowledge/embed.rs`) for dev/MVP cosine search; swap for a model-generated pipeline in production.
+- Keyword search remains available via `mode=keyword` on `/flow-studio/knowledge/search`.
+
+## Recommendation for production
+
+1. Dev Docker uses `pgvector/pgvector:pg17` (`docker-compose.yml`) so migration 0032 applies cleanly.
+2. Add an embedding worker (or inline on ingest) to populate `flow_knowledge_embeddings.embedding`.
+3. Run `cargo test -p buzz-db -- --ignored flow_studio_read_model_is_confined_to_community` against a migrated DB.
diff --git a/docs/MERGE_NOTES.md b/docs/MERGE_NOTES.md
new file mode 100644
index 00000000000..672133e4c89
--- /dev/null
+++ b/docs/MERGE_NOTES.md
@@ -0,0 +1,50 @@
+# Buzz Hive — Merge Notes
+
+> Branch: `feat/buzz-hive-p0` · Started: 2026-08-17
+
+## Upstream references (no local vendored copies)
+
+| Upstream | Buzz target |
+|---|---|
+| [simstudioai/sim](https://github.com/simstudioai/sim) | `crates/buzz-flow`, `desktop/src/features/flow-studio` |
+| [Ngxba/claude-code-cli-ui](https://github.com/Ngxba/claude-code-cli-ui) | `crates/buzz-agent-studio`, `desktop/src/features/agent-studio` |
+| [block/buzz](https://github.com/block/buzz) | core relay, auth, workflow engine |
+
+Design specs: `docs/BUZZ_HIVE_MERGE_SPEC.md`, `docs/BUZZ_HIVE_IMPLEMENTATION_PLAN.md`.
+
+## Nostr kind allocation
+
+| Module | Range | Registry |
+|---|---|---|
+| Flow Studio | 46200–46399 | `crates/buzz-core/src/kind.rs` |
+| Agent Studio | 47200–47399 | `crates/buzz-core/src/kind.rs` |
+
+## Implementation status — complete (in-repo P0–P5)
+
+| Phase | Status |
+|---|---|
+| P0 Skeleton | ✅ crates, kinds, migration 0032, ingest, NOTICE, docker pgvector |
+| P1 Agent Studio | ✅ graph, skills, telemetry 47300, UI |
+| P2 Flow Studio | ✅ canvas, YAML run, graph save/load, inline approval |
+| P3 Knowledge/Tables/Files | ✅ projector, semantic + keyword search, CRUD panels, isolation test |
+| P4 Cost monitor | ✅ `/agent-studio/costs` (ACP + Flow block rollup via kind 46201) |
+| P5 Docs & smoke | ✅ VISION.md, E2E smoke (`hive-studio.spec.ts`), schema.sql parity |
+
+## Ops-only (outside this repo)
+
+- Fork `buzz-hive` git remote + release tag `v0.1.0-buzz-hive`
+- Replace hash embeddings with a production embedding model when ready
+
+## Key paths
+
+```
+crates/buzz-flow/
+crates/buzz-agent-studio/
+crates/buzz-relay/src/api/flow_studio.rs
+crates/buzz-relay/src/api/agent_studio.rs
+crates/buzz-db/src/flow_studio.rs
+desktop/src/features/flow-studio/
+desktop/src/features/agent-studio/
+desktop/src-tauri/src/commands/hive_studio.rs
+migrations/0032_buzz_hive_studio.sql
+```
diff --git a/docs/WF-08.md b/docs/WF-08.md
new file mode 100644
index 00000000000..1bb05ae9a49
--- /dev/null
+++ b/docs/WF-08.md
@@ -0,0 +1,45 @@
+# WF-08 — Workflow approval gate persistence
+
+> Status: **implemented** on `feat/buzz-hive-p0`
+
+## Problem
+
+`buzz-workflow` could suspend at `RequestApproval` steps (`StepResult::Suspended`) but
+`WorkflowEngine::finalize_run()` marked runs **Failed** with `approval_not_supported`
+instead of persisting an approval record and setting `RunStatus::WaitingApproval`.
+
+Relay grant/deny handlers (`handle_approval_grant` / `handle_approval_deny` in
+`buzz-relay/src/handlers/command_executor.rs`) and `buzz-db::create_approval` already
+existed — only the executor → finalize wiring was missing.
+
+## Fix
+
+1. **`executor.rs`**: `StepResult::Suspended` carries `step_id`, `approver_spec`, `expires_at`.
+2. **`ExecutionResult`**: replaces `approval_token: Option` with `approval_gate: Option`.
+3. **`finalize_run()`** (`lib.rs`):
+ - Loads run → `workflow_id`
+ - Calls `db.create_approval(CreateApprovalParams { ... })`
+ - Updates run to `WaitingApproval` with suspended trace entry
+4. **Resume path** unchanged: grant handler spawns `execute_from_step` at `step_index + 1`.
+
+## YAML example
+
+```yaml
+steps:
+ - id: approve
+ action: request_approval
+ from: "@release-manager"
+ message: "Deploy to production?"
+ timeout: 24h
+```
+
+## Follow-ups (not WF-08)
+
+- Emit kind `46010` approval-requested Nostr event on suspend (relay integration).
+- Role-based approver specs (`@engineering-lead`) — relay currently requires 64-char hex pubkey.
+- Long `Delay` steps (>270s) — use scheduled resume (WF-09).
+
+## Tests
+
+- `cargo test -p buzz-workflow` — schema + executor duration parsing
+- Integration: `crates/buzz-test-client/tests/` approval conformance (`approval_token_is_community_confined`)
diff --git a/migrations/0032_buzz_hive_studio.sql b/migrations/0032_buzz_hive_studio.sql
new file mode 100644
index 00000000000..710f5ab3656
--- /dev/null
+++ b/migrations/0032_buzz_hive_studio.sql
@@ -0,0 +1,61 @@
+-- Buzz Hive Flow Studio read-model tables (P3 projector target).
+-- Source of truth remains Nostr events (kinds 46250–46399).
+
+CREATE EXTENSION IF NOT EXISTS vector;
+
+CREATE TABLE IF NOT EXISTS flow_knowledge_documents (
+ community_id UUID NOT NULL,
+ document_id TEXT NOT NULL,
+ knowledge_base_id TEXT NOT NULL,
+ filename TEXT NOT NULL,
+ mime_type TEXT NOT NULL,
+ token_count INTEGER NOT NULL DEFAULT 0,
+ ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (community_id, document_id)
+);
+
+CREATE INDEX IF NOT EXISTS flow_kb_docs_community_idx
+ ON flow_knowledge_documents (community_id, knowledge_base_id);
+
+CREATE TABLE IF NOT EXISTS flow_knowledge_embeddings (
+ community_id UUID NOT NULL,
+ embedding_id TEXT NOT NULL,
+ document_id TEXT NOT NULL,
+ chunk_index INTEGER NOT NULL,
+ content TEXT NOT NULL,
+ embedding vector(1536) NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (community_id, embedding_id)
+);
+
+CREATE INDEX IF NOT EXISTS flow_kb_embeddings_document_idx
+ ON flow_knowledge_embeddings (community_id, document_id);
+
+CREATE TABLE IF NOT EXISTS flow_table_rows (
+ community_id UUID NOT NULL,
+ table_id TEXT NOT NULL,
+ row_id TEXT NOT NULL,
+ row_json JSONB NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ deleted_at TIMESTAMPTZ,
+ PRIMARY KEY (community_id, table_id, row_id)
+);
+
+CREATE INDEX IF NOT EXISTS flow_table_rows_table_idx
+ ON flow_table_rows (community_id, table_id)
+ WHERE deleted_at IS NULL;
+
+CREATE TABLE IF NOT EXISTS flow_files (
+ community_id UUID NOT NULL,
+ file_id TEXT NOT NULL,
+ filename TEXT NOT NULL,
+ media_url TEXT,
+ version INTEGER NOT NULL DEFAULT 1,
+ deleted_at TIMESTAMPTZ,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (community_id, file_id)
+);
+
+CREATE INDEX IF NOT EXISTS flow_files_community_idx
+ ON flow_files (community_id)
+ WHERE deleted_at IS NULL;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c8be559938c..1464a9395f9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -174,6 +174,9 @@ importers:
'@tiptap/starter-kit':
specifier: ^3.22.3
version: 3.22.5
+ '@xyflow/react':
+ specifier: ^12.6.4
+ version: 12.11.3(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -2049,6 +2052,24 @@ packages:
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+ '@types/d3-color@3.1.3':
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+ '@types/d3-drag@3.0.7':
+ resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
+
+ '@types/d3-interpolate@3.0.4':
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+ '@types/d3-selection@3.0.11':
+ resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
+
+ '@types/d3-transition@3.0.9':
+ resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
+
+ '@types/d3-zoom@3.0.8':
+ resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
+
'@types/debug@4.1.13':
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
@@ -2154,6 +2175,22 @@ packages:
'@vitest/utils@4.1.10':
resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==}
+ '@xyflow/react@12.11.3':
+ resolution: {integrity: sha512-G3jogHz2GWUtIOkhavUGno2YzY9u6fILIJBttfsBendb0/HWB90JG+sOTAvlIMEwyvq9zgy9V9ZQSwyQjR5QzQ==}
+ peerDependencies:
+ '@types/react': '>=17'
+ '@types/react-dom': '>=17'
+ react: '>=17'
+ react-dom: '>=17'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@xyflow/system@0.0.80':
+ resolution: {integrity: sha512-ywc3ZqG91brzWrH1WlwMdIX4goOfrpBy6AbLdVSaof/Xx9l138ijIKRExM6EkMro2F+OImGmSiA/WKcXvKVcfA==}
+
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
@@ -2276,6 +2313,9 @@ packages:
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+ classcat@5.0.5:
+ resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
+
classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
@@ -2329,6 +2369,44 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+ d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+
+ d3-dispatch@3.0.1:
+ resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
+ engines: {node: '>=12'}
+
+ d3-drag@3.0.0:
+ resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
+ engines: {node: '>=12'}
+
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
+ d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+
+ d3-selection@3.0.0:
+ resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
+ engines: {node: '>=12'}
+
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
+ d3-transition@3.0.1:
+ resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ d3-selection: 2 - 3
+
+ d3-zoom@3.0.0:
+ resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
+ engines: {node: '>=12'}
+
data-urls@6.0.1:
resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==}
engines: {node: '>=20'}
@@ -3731,6 +3809,21 @@ packages:
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
+ zustand@4.5.7:
+ resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
+ engines: {node: '>=12.7.0'}
+ peerDependencies:
+ '@types/react': '>=16.8'
+ immer: '>=9.0.6'
+ react: '>=16.8'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -5301,6 +5394,27 @@ snapshots:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
+ '@types/d3-color@3.1.3': {}
+
+ '@types/d3-drag@3.0.7':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-interpolate@3.0.4':
+ dependencies:
+ '@types/d3-color': 3.1.3
+
+ '@types/d3-selection@3.0.11': {}
+
+ '@types/d3-transition@3.0.9':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-zoom@3.0.8':
+ dependencies:
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-selection': 3.0.11
+
'@types/debug@4.1.13':
dependencies:
'@types/ms': 2.1.0
@@ -5407,6 +5521,31 @@ snapshots:
convert-source-map: 2.0.0
tinyrainbow: 3.1.0
+ '@xyflow/react@12.11.3(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ dependencies:
+ '@xyflow/system': 0.0.80
+ classcat: 5.0.5
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ zustand: 4.5.7(@types/react@19.2.18)(react@19.2.8)
+ optionalDependencies:
+ '@types/react': 19.2.18
+ '@types/react-dom': 19.2.4(@types/react@19.2.18)
+ transitivePeerDependencies:
+ - immer
+
+ '@xyflow/system@0.0.80':
+ dependencies:
+ '@types/d3-drag': 3.0.7
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-selection': 3.0.11
+ '@types/d3-transition': 3.0.9
+ '@types/d3-zoom': 3.0.8
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-zoom: 3.0.0
+
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
@@ -5520,6 +5659,8 @@ snapshots:
dependencies:
clsx: 2.1.1
+ classcat@5.0.5: {}
+
classnames@2.5.1: {}
clean-git-ref@2.0.1: {}
@@ -5564,6 +5705,42 @@ snapshots:
csstype@3.2.3: {}
+ d3-color@3.1.0: {}
+
+ d3-dispatch@3.0.1: {}
+
+ d3-drag@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-selection: 3.0.0
+
+ d3-ease@3.0.1: {}
+
+ d3-interpolate@3.0.1:
+ dependencies:
+ d3-color: 3.1.0
+
+ d3-selection@3.0.0: {}
+
+ d3-timer@3.0.1: {}
+
+ d3-transition@3.0.1(d3-selection@3.0.0):
+ dependencies:
+ d3-color: 3.1.0
+ d3-dispatch: 3.0.1
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-timer: 3.0.1
+
+ d3-zoom@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
data-urls@6.0.1:
dependencies:
whatwg-mimetype: 5.0.0
@@ -7135,4 +7312,11 @@ snapshots:
zod@4.4.3: {}
+ zustand@4.5.7(@types/react@19.2.18)(react@19.2.8):
+ dependencies:
+ use-sync-external-store: 1.6.0(react@19.2.8)
+ optionalDependencies:
+ '@types/react': 19.2.18
+ react: 19.2.8
+
zwitch@2.0.4: {}
diff --git a/preview-features.json b/preview-features.json
index 388f1c39b04..817c0e2a223 100644
--- a/preview-features.json
+++ b/preview-features.json
@@ -30,6 +30,18 @@
"name": "Agent-managed profiles",
"description": "Let agents manage their own relay name and avatar instead of restoring the desktop copy",
"platforms": ["desktop"]
+ },
+ {
+ "id": "flow-studio",
+ "name": "Flow Studio",
+ "description": "Visual workflow builder (Buzz Hive / Sim merge)",
+ "platforms": ["desktop"]
+ },
+ {
+ "id": "agent-studio",
+ "name": "Agent Studio",
+ "description": "Agent/skill dependency graph and GitHub import (Buzz Hive)",
+ "platforms": ["desktop"]
}
]
}
diff --git a/schema/schema.sql b/schema/schema.sql
index 9ef7bc0a4b8..f556ed15516 100644
--- a/schema/schema.sql
+++ b/schema/schema.sql
@@ -22,6 +22,7 @@
-- 4. Operator-global tables are named in the explicit allowlist, not implied.
CREATE EXTENSION IF NOT EXISTS pgcrypto;
+CREATE EXTENSION IF NOT EXISTS vector;
-- ── Custom types ──────────────────────────────────────────────────────────────
@@ -1378,6 +1379,66 @@ CREATE TABLE community_deletion_executor_heartbeats (
draining BOOLEAN NOT NULL DEFAULT false,
stopped_at TIMESTAMPTZ
);
+
+-- ── Buzz Hive Flow Studio read model (pgvector semantic search) ───────────────
+
+CREATE TABLE flow_knowledge_documents (
+ community_id UUID NOT NULL,
+ document_id TEXT NOT NULL,
+ knowledge_base_id TEXT NOT NULL,
+ filename TEXT NOT NULL,
+ mime_type TEXT NOT NULL,
+ token_count INTEGER NOT NULL DEFAULT 0,
+ ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (community_id, document_id)
+);
+
+CREATE INDEX flow_kb_docs_community_idx
+ ON flow_knowledge_documents (community_id, knowledge_base_id);
+
+CREATE TABLE flow_knowledge_embeddings (
+ community_id UUID NOT NULL,
+ embedding_id TEXT NOT NULL,
+ document_id TEXT NOT NULL,
+ chunk_index INTEGER NOT NULL,
+ content TEXT NOT NULL,
+ embedding vector(1536) NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (community_id, embedding_id)
+);
+
+CREATE INDEX flow_kb_embeddings_document_idx
+ ON flow_knowledge_embeddings (community_id, document_id);
+
+CREATE TABLE flow_table_rows (
+ community_id UUID NOT NULL,
+ table_id TEXT NOT NULL,
+ row_id TEXT NOT NULL,
+ row_json JSONB NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ deleted_at TIMESTAMPTZ,
+ PRIMARY KEY (community_id, table_id, row_id)
+);
+
+CREATE INDEX flow_table_rows_table_idx
+ ON flow_table_rows (community_id, table_id)
+ WHERE deleted_at IS NULL;
+
+CREATE TABLE flow_files (
+ community_id UUID NOT NULL,
+ file_id TEXT NOT NULL,
+ filename TEXT NOT NULL,
+ media_url TEXT,
+ version INTEGER NOT NULL DEFAULT 1,
+ deleted_at TIMESTAMPTZ,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (community_id, file_id)
+);
+
+CREATE INDEX flow_files_community_idx
+ ON flow_files (community_id)
+ WHERE deleted_at IS NULL;
+
INSERT INTO _operator_global_tables (table_name, reason) VALUES
('community_deletion_requests', 'deployment deletion lifecycle and frozen inventory'),
('community_deletion_approvals', 'deployment operator destructive approvals'),