diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..ba2dfea1 --- /dev/null +++ b/.env.example @@ -0,0 +1,89 @@ +# Copy to .env and fill in. Never commit the filled copy. +# Compose reads it from this directory. +# The image already sets BRAID_HOME, the port, and the Studio assets. +# packages/server/README.md documents every variable the server reads. + +# --------------------------------------------------------------------------- +# required +# --------------------------------------------------------------------------- + +# server: the name users type, and the origin the API answers on. +# Both Google redirects derive from it, so register each in the console. +# /auth/google/callback signing in +# /oauth/google/callback connecting a Drive source +# Serving anybody else means putting the name they type here. +BRAID_STUDIO_URL=http://localhost:4321 + +# server: who may create workspaces and manage the roster. +# Comma separated, at least one. +BRAID_ADMIN_EMAILS= + +# server signs people in with these. +# source-loader-gdrive connects a Drive source with them too. +BRAID_GOOGLE_CLIENT_ID= +BRAID_GOOGLE_CLIENT_SECRET= + +# agent-claude-code: one subscription seat, from `claude setup-token`. +# Every run draws on it unless that person saved their own. +# ANTHROPIC_API_KEY set alongside it wins, and the seat idles. +CLAUDE_CODE_OAUTH_TOKEN= + +# --------------------------------------------------------------------------- +# optional +# --------------------------------------------------------------------------- +# Leaving any of these empty turns the feature off. + +# server: restricts sign-in to these domains, comma separated. +# Removing one closes the browser door and the MCP endpoint at once. +BRAID_ALLOWED_DOMAINS= + +# source-loader-git: read access for a private remote. +# PRODUCT.md references it as `oauth2:${GIT_TOKEN}@host`, never the value. +GIT_TOKEN= + +# source-loader-github: an Issues source, connected per source rather than +# by a token, so the issues a person can see are the ones Braid mirrors. +# Register the callback on the GitHub App, and install the App on every +# repository it reads, since an uninstalled one answers with nothing. +# /oauth/github/callback +# BRAID_GITHUB_CLIENT_ID= +# BRAID_GITHUB_CLIENT_SECRET= + +# server: sign-in through your own authorization server. +# An issuer displaces the Google client above and turns on the MCP endpoint. +# Its tokens must carry an `email` claim, which is what names a Braid user. +# BRAID_OIDC_ISSUER=https://keycloak.your-host/realms/braid +# BRAID_OIDC_CLIENT_ID=braid-studio +# BRAID_OIDC_CLIENT_SECRET= +# BRAID_OIDC_AUDIENCE defaults to the API url. +# compose.yaml omits it, since an empty value would not read as unset. + +# server: the read-only MCP endpoint, once an issuer exists above. +# It exchanges each caller's token, so history records who asked. +# BRAID_MCP_GATEWAY_CLIENT_ID= +# BRAID_MCP_GATEWAY_CLIENT_SECRET= + +# compose.oidc.yaml runs Keycloak as one worked example, and +# keycloak/init.sh provisions the realm with Google behind it. +# These four are that example's own, not Braid's. +# KC_BOOTSTRAP_ADMIN_PASSWORD= +# KEYCLOAK_URL=http://localhost:8080 +# KEYCLOAK_API_SECRET= +# KEYCLOAK_GATEWAY_SECRET= +# Narrows Google to one workspace domain. Empty lets any account in. +# GOOGLE_HOSTED_DOMAIN= + +# server: per-user agent credentials, from `openssl rand -base64 32`. +# Each person then saves their own under Settings, and their runs spend it. +# Without it nothing is stored, since a derivable key protects nothing. +# BRAID_SECRET_KEY= +# The key being rotated out, read and rewritten under the current one. +# BRAID_SECRET_KEY_PREVIOUS= + +# server: search by meaning. The address is the switch. +# Unset, the graph is searched by name and description alone. +# Any OpenAI-compatible endpoint, Ollama included. Model defaults to bge-m3. +# The server appends `/v1/embeddings`, so name the origin and stop there. +# BRAID_EMBEDDING_HOST=http://ollama:11434 +# BRAID_EMBEDDING_MODEL=bge-m3:latest +# BRAID_EMBEDDING_API_KEY= diff --git a/architecture.pen b/.github/assets/architecture.pen similarity index 81% rename from architecture.pen rename to .github/assets/architecture.pen index 339407ac..caa3d86e 100644 --- a/architecture.pen +++ b/.github/assets/architecture.pen @@ -1,5 +1,5 @@ { - "version": "2.15", + "version": "2.17", "children": [ { "type": "frame", @@ -9,7 +9,7 @@ "name": "Braid Architecture", "clip": true, "width": 1489, - "height": 671, + "height": 652, "fill": "$bg", "layout": "none", "children": [ @@ -17,10 +17,10 @@ "type": "frame", "id": "A6l35K", "x": 24, - "y": 159, - "name": "Z:SERVER · COMPOSITION ROOT · REST + SSE API · OPENAPI · AUTH", + "y": 182, + "name": "Z:SERVER", "width": 1440, - "height": 488, + "height": 446, "fill": "#fdfcfa", "cornerRadius": 18, "stroke": "$border", @@ -55,97 +55,12 @@ }, { "type": "frame", - "id": "w3CUId", - "x": 58, - "y": 436, - "name": "Z:LOADERS", - "width": 260, - "height": 124, - "fill": "$card2", - "cornerRadius": 14, - "stroke": "$border", - "strokeWidth": 1, - "layout": "none", - "children": [ - { - "type": "rectangle", - "cornerRadius": 2, - "id": "hCPfO", - "x": 18, - "y": 18, - "name": "dot", - "fill": "$accent", - "width": 6, - "height": 6 - }, - { - "type": "text", - "id": "aRup5", - "x": 34, - "y": 13, - "name": "lbl", - "fill": "$sub", - "content": "LOADERS", - "fontFamily": "$font-mono", - "fontSize": 11.5, - "fontWeight": "600", - "letterSpacing": 1 - } - ] - }, - { - "type": "frame", - "id": "Hb3FI", - "x": 78, - "y": 477, - "name": "B:Source Loaders", - "width": 220, - "height": 56, - "fill": "$card", - "cornerRadius": 9, - "stroke": "$border", - "strokeWidth": 1, - "layout": "vertical", - "gap": 3, - "padding": [ - 9, - 12 - ], - "justifyContent": "center", - "children": [ - { - "type": "text", - "id": "gbEG4", - "name": "t", - "fill": "$ink", - "content": "Source Loaders", - "fontFamily": "$font-sans", - "fontSize": 14, - "fontWeight": "700" - }, - { - "type": "text", - "id": "Y3M3WU", - "name": "s", - "fill": "$sub", - "textGrowth": "fixed-width", - "width": "fill_container", - "content": "git · github · gdrive", - "lineHeight": 1.25, - "fontFamily": "$font-sans", - "fontSize": 11.5, - "fontWeight": "normal" - } - ] - }, - { - "type": "frame", - "id": "B4SmI", - "x": 350, - "y": 219, - "name": "Z:ENGINE · THE LOOP", - "width": 496, - "height": 273, + "id": "YSXlU", + "x": 24, + "y": 24, + "name": "Z:SURFACES", + "width": 1440, + "height": 118, "fill": "$card2", "cornerRadius": 14, "stroke": "$border", @@ -155,7 +70,7 @@ { "type": "rectangle", "cornerRadius": 2, - "id": "RT1yf", + "id": "f2veO", "x": 18, "y": 18, "name": "dot", @@ -165,12 +80,12 @@ }, { "type": "text", - "id": "TeifT", + "id": "RRzyp", "x": 32, "y": 12, "name": "lbl", "fill": "$sub", - "content": "ENGINE · HITL LOOP", + "content": "SURFACES", "fontFamily": "$font-mono", "fontSize": 11.5, "fontWeight": "600", @@ -182,10 +97,10 @@ "type": "frame", "id": "zNNuF", "x": 58, - "y": 219, + "y": 236, "name": "Z:SOURCES", "width": 260, - "height": 184, + "height": 358, "fill": "$card2", "cornerRadius": 14, "stroke": "$border", @@ -220,12 +135,12 @@ }, { "type": "frame", - "id": "YSXlU", - "x": 25, - "y": 24, - "name": "Z:SURFACES", - "width": 1440, - "height": 101, + "id": "B4SmI", + "x": 350, + "y": 236, + "name": "Z:ENGINE", + "width": 496, + "height": 358, "fill": "$card2", "cornerRadius": 14, "stroke": "$border", @@ -235,7 +150,7 @@ { "type": "rectangle", "cornerRadius": 2, - "id": "f2veO", + "id": "RT1yf", "x": 18, "y": 18, "name": "dot", @@ -245,12 +160,12 @@ }, { "type": "text", - "id": "RRzyp", + "id": "TeifT", "x": 32, "y": 12, "name": "lbl", "fill": "$sub", - "content": "SURFACES", + "content": "ENGINE · HITL LOOP", "fontFamily": "$font-mono", "fontSize": 11.5, "fontWeight": "600", @@ -262,10 +177,10 @@ "type": "frame", "id": "mzTlB", "x": 878, - "y": 219, - "name": "Z:KNOWLEDGE GRAPH", + "y": 236, + "name": "Z:MODEL", "width": 260, - "height": 273, + "height": 358, "fill": "$card2", "cornerRadius": 14, "stroke": "$border", @@ -302,10 +217,10 @@ "type": "frame", "id": "Mzhev", "x": 1170, - "y": 219, - "name": "Z:VIEWS", + "y": 236, + "name": "Z:READS", "width": 260, - "height": 184, + "height": 358, "fill": "$card2", "cornerRadius": 14, "stroke": "$border", @@ -340,51 +255,11 @@ }, { "type": "frame", - "id": "oETru", - "x": 350, - "y": 526, - "name": "Z:HISTORY", - "width": 788, - "height": 109, - "fill": "$card2", - "cornerRadius": 14, - "stroke": "$border", - "strokeWidth": 1, - "layout": "none", - "children": [ - { - "type": "rectangle", - "cornerRadius": 2, - "id": "bnA2L", - "x": 18, - "y": 18, - "name": "dot", - "fill": "$accent", - "width": 6, - "height": 6 - }, - { - "type": "text", - "id": "j3nKQR", - "x": 32, - "y": 12, - "name": "lbl", - "fill": "$sub", - "content": "HISTORY", - "fontFamily": "$font-mono", - "fontSize": 11.5, - "fontWeight": "600", - "letterSpacing": 1 - } - ] - }, - { - "type": "frame", - "id": "gbvoJ", - "x": 78, - "y": 261, - "name": "B:Intent", - "width": 220, + "id": "fcoSN", + "x": 44, + "y": 66, + "name": "B:Studio", + "width": 335, "height": 56, "fill": "$card", "cornerRadius": 9, @@ -393,29 +268,29 @@ "layout": "vertical", "gap": 3, "padding": [ - 9, - 12 + 8, + 14 ], "justifyContent": "center", "children": [ { "type": "text", - "id": "VXlUo", + "id": "H4xD4", "name": "t", "fill": "$ink", - "content": "Intent", + "content": "Studio", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "V7hcI", + "id": "qbgZF", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "PRDs, RFCs, issues", + "content": "the web UI, the primary control surface", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -425,11 +300,11 @@ }, { "type": "frame", - "id": "W1IhH9", - "x": 78, - "y": 333, - "name": "B:Code", - "width": 220, + "id": "qGxoF", + "x": 399, + "y": 66, + "name": "B:Desktop", + "width": 335, "height": 56, "fill": "$card", "cornerRadius": 9, @@ -438,29 +313,29 @@ "layout": "vertical", "gap": 3, "padding": [ - 9, - 12 + 8, + 14 ], "justifyContent": "center", "children": [ { "type": "text", - "id": "X6SvFl", + "id": "dlRKo", "name": "t", "fill": "$ink", - "content": "Code", + "content": "Desktop", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "CX5Rs", + "id": "r5ds2H", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "repositories, microservices", + "content": "the Studio wrapper, sidecar or remote", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -470,11 +345,11 @@ }, { "type": "frame", - "id": "KIOll", - "x": 1190, - "y": 333, - "name": "B:Views", - "width": 220, + "id": "i9FRt1", + "x": 754, + "y": 66, + "name": "B:CLI", + "width": 335, "height": 56, "fill": "$card", "cornerRadius": 9, @@ -483,29 +358,29 @@ "layout": "vertical", "gap": 3, "padding": [ - 7, + 8, 14 ], "justifyContent": "center", "children": [ { "type": "text", - "id": "Vpt8i", + "id": "fJWuS", "name": "t", "fill": "$ink", - "content": "Views", + "content": "CLI", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "FPG4X", + "id": "WNJ8F", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "organized artifacts, e.g., docs", + "content": "scaffold a workspace, boot the stack", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -515,11 +390,11 @@ }, { "type": "frame", - "id": "hcu1L", - "x": 1190, - "y": 261, - "name": "B:Ask", - "width": 220, + "id": "n2LgXD", + "x": 1109, + "y": 66, + "name": "B:MCP Clients", + "width": 335, "height": 56, "fill": "$card", "cornerRadius": 9, @@ -528,29 +403,29 @@ "layout": "vertical", "gap": 3, "padding": [ - 7, + 8, 14 ], "justifyContent": "center", "children": [ { "type": "text", - "id": "ytyEE", + "id": "i95KMN", "name": "t", "fill": "$ink", - "content": "Ask", + "content": "MCP Clients", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "VhV6u", + "id": "mkHXu", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "one-time ask for understanding", + "content": "external clients reading the graph", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -560,10 +435,10 @@ }, { "type": "frame", - "id": "j2rWS", - "x": 898, - "y": 421, - "name": "B:Storage", + "id": "Hb3FI", + "x": 78, + "y": 278, + "name": "B:Source Loaders", "width": 220, "height": 56, "fill": "$card", @@ -580,22 +455,22 @@ "children": [ { "type": "text", - "id": "MjUZE", + "id": "gbEG4", "name": "t", "fill": "$ink", - "content": "Storage", + "content": "Source Loaders", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "T46fUY", + "id": "Y3M3WU", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "the model store, e.g., kùzu", + "content": "git · github · gdrive · mcp", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -605,11 +480,11 @@ }, { "type": "frame", - "id": "v0eR2", + "id": "YeCxs", "x": 370, - "y": 566, - "name": "B:Git History", - "width": 220, + "y": 278, + "name": "B:Agent", + "width": 456, "height": 56, "fill": "$card", "cornerRadius": 9, @@ -625,22 +500,22 @@ "children": [ { "type": "text", - "id": "P99FE", + "id": "jbMhB", "name": "t", "fill": "$ink", - "content": "Git", + "content": "Agent", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "whEDJ", + "id": "KCuJe", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "the versioned workspace tree", + "content": "the pluggable agent runtime, e.g., claude code, codex, gemini cli, or custom", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -650,10 +525,10 @@ }, { "type": "frame", - "id": "YeCxs", + "id": "FJVbM", "x": 370, - "y": 261, - "name": "B:Agent", + "y": 358, + "name": "B:Skills", "width": 456, "height": 56, "fill": "$card", @@ -670,22 +545,22 @@ "children": [ { "type": "text", - "id": "jbMhB", + "id": "vNZCX", "name": "t", "fill": "$ink", - "content": "Agent", + "content": "Skills", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "KCuJe", + "id": "KDcB1", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "the pluggable agent runtime, e.g., claude code, codex, gemini cli, or custom", + "content": "ask · build · generate, each handed only its own tools", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -695,15 +570,15 @@ }, { "type": "frame", - "id": "FJVbM", + "id": "GH4xf", "x": 370, - "y": 341, - "name": "B:Skills", - "width": 456, + "y": 438, + "name": "B:Clarification", + "width": 220, "height": 56, - "fill": "$card", + "fill": "$peach", "cornerRadius": 9, - "stroke": "$border", + "stroke": "$peach-border", "strokeWidth": 1, "layout": "vertical", "gap": 3, @@ -715,22 +590,22 @@ "children": [ { "type": "text", - "id": "vNZCX", + "id": "GhOAE", "name": "t", "fill": "$ink", - "content": "Actions (Skills)", + "content": "Clarification", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "KDcB1", + "id": "ao65Z", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "the agent's skills, e.g., ask, build (based on ontology), generate (view)", + "content": "a question waiting in the Inbox", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -742,8 +617,8 @@ "type": "frame", "id": "Fxrt9", "x": 606, - "y": 421, - "name": "B:Proposals + Clarifications", + "y": 438, + "name": "B:Proposal", "width": 220, "height": 56, "fill": "$peach", @@ -775,7 +650,7 @@ "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "a proposed change to the model", + "content": "a change waiting in the Inbox", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -785,10 +660,10 @@ }, { "type": "frame", - "id": "xnxGk", + "id": "jG3Qr", "x": 898, - "y": 341, - "name": "B:Model", + "y": 278, + "name": "B:Ontology", "width": 220, "height": 56, "fill": "$card", @@ -805,22 +680,22 @@ "children": [ { "type": "text", - "id": "viJP4", + "id": "iv7PL", "name": "t", "fill": "$ink", - "content": "Graph", + "content": "Ontology", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "F4Pqro", + "id": "FHc1V", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "the single-source-of-truth", + "content": "the modeling system, e.g., ddd", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -830,10 +705,10 @@ }, { "type": "frame", - "id": "jG3Qr", + "id": "xnxGk", "x": 898, - "y": 261, - "name": "B:Ontology", + "y": 358, + "name": "B:Graph", "width": 220, "height": 56, "fill": "$card", @@ -850,22 +725,22 @@ "children": [ { "type": "text", - "id": "iv7PL", + "id": "viJP4", "name": "t", "fill": "$ink", - "content": "Ontology", + "content": "Graph", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "FHc1V", + "id": "F4Pqro", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "the modeling system, e.g., ddd", + "content": "the single source of truth", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -875,11 +750,11 @@ }, { "type": "frame", - "id": "fcoSN", - "x": 45, - "y": 58, - "name": "B:Studio", - "width": 335, + "id": "j2rWS", + "x": 898, + "y": 438, + "name": "B:Storage", + "width": 220, "height": 56, "fill": "$card", "cornerRadius": 9, @@ -888,29 +763,29 @@ "layout": "vertical", "gap": 3, "padding": [ - 8, - 14 + 9, + 12 ], "justifyContent": "center", "children": [ { "type": "text", - "id": "H4xD4", + "id": "MjUZE", "name": "t", "fill": "$ink", - "content": "Studio", + "content": "Storage", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "qbgZF", + "id": "T46fUY", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "web UI, the primary control surface", + "content": "the model store, e.g., kùzu", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -920,11 +795,11 @@ }, { "type": "frame", - "id": "qGxoF", - "x": 400, - "y": 58, - "name": "B:Desktop", - "width": 335, + "id": "hcu1L", + "x": 1190, + "y": 358, + "name": "B:Ask", + "width": 220, "height": 56, "fill": "$card", "cornerRadius": 9, @@ -933,29 +808,29 @@ "layout": "vertical", "gap": 3, "padding": [ - 8, + 7, 14 ], "justifyContent": "center", "children": [ { "type": "text", - "id": "dlRKo", + "id": "ytyEE", "name": "t", "fill": "$ink", - "content": "Desktop", + "content": "Ask", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "r5ds2H", + "id": "VhV6u", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "Studio wrapper, sidecar or remote server", + "content": "a question a run answers", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -965,11 +840,11 @@ }, { "type": "frame", - "id": "i9FRt1", - "x": 755, - "y": 58, - "name": "B:CLI", - "width": 335, + "id": "KIOll", + "x": 1190, + "y": 438, + "name": "B:Views", + "width": 220, "height": 56, "fill": "$card", "cornerRadius": 9, @@ -978,29 +853,29 @@ "layout": "vertical", "gap": 3, "padding": [ - 8, + 7, 14 ], "justifyContent": "center", "children": [ { "type": "text", - "id": "fJWuS", + "id": "Vpt8i", "name": "t", "fill": "$ink", - "content": "CLI", + "content": "Views", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "WNJ8F", + "id": "FPG4X", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "terminal (scriptable, headless runs)", + "content": "one artifact per reason to read", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -1010,11 +885,11 @@ }, { "type": "frame", - "id": "n2LgXD", - "x": 1110, - "y": 58, - "name": "B:MCP clients", - "width": 335, + "id": "mEXuA", + "x": 1190, + "y": 518, + "name": "B:View Generators", + "width": 220, "height": 56, "fill": "$card", "cornerRadius": 9, @@ -1023,29 +898,119 @@ "layout": "vertical", "gap": 3, "padding": [ - 8, - 14 + 9, + 12 ], "justifyContent": "center", "children": [ { "type": "text", - "id": "i95KMN", + "id": "MGTMU", "name": "t", "fill": "$ink", - "content": "MCP Clients", + "content": "View Generators", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "mkHXu", + "id": "FNu1B", + "name": "s", + "fill": "$sub", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "one projection, one skill per form", + "lineHeight": 1.25, + "fontFamily": "$font-sans", + "fontSize": 11.5, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "v0eR2", + "x": 370, + "y": 518, + "name": "B:Git", + "width": 456, + "height": 56, + "fill": "$card", + "cornerRadius": 9, + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 3, + "padding": [ + 9, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "P99FE", + "name": "t", + "fill": "$ink", + "content": "Git", + "fontFamily": "$font-sans", + "fontSize": 14, + "fontWeight": "700" + }, + { + "type": "text", + "id": "whEDJ", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "external clients via openapi-mcp-gateway", + "content": "the versioned workspace tree", + "lineHeight": 1.25, + "fontFamily": "$font-sans", + "fontSize": 11.5, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "y1tnM", + "x": 898, + "y": 518, + "name": "B:Snapshot", + "width": 220, + "height": 56, + "fill": "$card", + "cornerRadius": 9, + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 3, + "padding": [ + 9, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "F9zdsP", + "name": "t", + "fill": "$ink", + "content": "Snapshot", + "fontFamily": "$font-sans", + "fontSize": 14, + "fontWeight": "700" + }, + { + "type": "text", + "id": "qqAdB", + "name": "s", + "fill": "$sub", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "the graph as model.json", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -1057,7 +1022,7 @@ "type": "frame", "id": "GkT3T", "x": 238, - "y": 488, + "y": 289, "name": "pill", "fill": "$pill", "cornerRadius": 3, @@ -1083,7 +1048,7 @@ "type": "frame", "id": "GjILq", "x": 764, - "y": 272, + "y": 289, "name": "pill", "fill": "$pill", "cornerRadius": 3, @@ -1109,7 +1074,7 @@ "type": "frame", "id": "t30Ri", "x": 1062, - "y": 271, + "y": 288, "name": "pill", "fill": "$pill", "cornerRadius": 3, @@ -1135,7 +1100,7 @@ "type": "frame", "id": "fMfHJ", "x": 1062, - "y": 431, + "y": 448, "name": "pill", "fill": "$pill", "cornerRadius": 3, @@ -1160,8 +1125,8 @@ { "type": "group", "id": "XYNxR", - "x": 1197, - "y": 174, + "x": 1196, + "y": 197, "children": [ { "type": "text", @@ -1215,129 +1180,71 @@ } ] }, - { - "type": "frame", - "id": "GH4xf", - "x": 370, - "y": 421, - "name": "B:Clarification", - "width": 220, - "height": 56, - "fill": "$peach", - "cornerRadius": 9, - "stroke": "$peach-border", - "strokeWidth": 1, - "layout": "vertical", - "gap": 3, - "padding": [ - 9, - 12 - ], - "justifyContent": "center", - "children": [ - { - "type": "text", - "id": "GhOAE", - "name": "t", - "fill": "$ink", - "content": "Clarification", - "fontFamily": "$font-sans", - "fontSize": 14, - "fontWeight": "700" - }, - { - "type": "text", - "id": "ao65Z", - "name": "s", - "fill": "$sub", - "textGrowth": "fixed-width", - "width": "fill_container", - "content": "a question to resolve ambiguity", - "lineHeight": 1.25, - "fontFamily": "$font-sans", - "fontSize": 11.5, - "fontWeight": "normal" - } - ] - }, { "type": "text", "id": "eJhaE", "x": 630, - "y": 136, + "y": 156, "name": "cl", - "fill": "$faint", + "fill": "$sub", "content": "requests", "fontFamily": "$font-sans", "fontSize": 11, - "fontWeight": "600" + "fontWeight": "normal" }, { "type": "text", "id": "aJ02i", "x": 806, - "y": 136, + "y": 156, "name": "cl", - "fill": "$faint", - "content": "API + SSE events", + "fill": "$sub", + "content": "REST · SSE · AG-UI", "fontFamily": "$font-sans", "fontSize": 11, - "fontWeight": "600" + "fontWeight": "normal" }, { "type": "text", "id": "ZSCCU", - "x": 1133, - "y": 377, + "x": 1132, + "y": 435, "name": "cl", - "fill": "$faint", + "fill": "$sub", "content": "projects", "fontFamily": "$font-sans", "fontSize": 11, - "fontWeight": "600" - }, - { - "type": "text", - "id": "dEeey", - "x": 78, - "y": 537, - "name": "reactor", - "fill": "$faint", - "content": "reactor re-syncs on change, opt-in", - "fontFamily": "$font-sans", - "fontSize": 10.5, - "fontWeight": "normal", - "fontStyle": "italic" + "fontWeight": "normal" }, { "type": "text", "id": "rjJBE", - "x": 1012, - "y": 322, + "x": 1019, + "y": 339, "name": "g_ol", - "fill": "$faint", + "fill": "$sub", "content": "builds on", "fontFamily": "$font-sans", - "fontSize": 10.5, - "fontWeight": "600" + "fontSize": 11, + "fontWeight": "normal" }, { "type": "text", "id": "m7ySt", - "x": 1012, - "y": 403, + "x": 1019, + "y": 419, "name": "g_kl", - "fill": "$faint", + "fill": "$sub", "content": "saves to", "fontFamily": "$font-sans", - "fontSize": 10.5, - "fontWeight": "600" + "fontSize": 11, + "fontWeight": "normal" }, { "type": "text", "id": "QM98r", - "x": 606, - "y": 322, + "x": 609, + "y": 339, "name": "cl", "fill": "$sub", "content": "runs", @@ -1348,11 +1255,11 @@ { "type": "text", "id": "BBwTL", - "x": 404, - "y": 403, + "x": 437, + "y": 419, "name": "cl", "fill": "$sub", - "content": "generates", + "content": "asks", "fontFamily": "$font-sans", "fontSize": 11, "fontWeight": "normal" @@ -1360,11 +1267,11 @@ { "type": "text", "id": "a5C3Q", - "x": 724, - "y": 403, + "x": 698, + "y": 419, "name": "cl", "fill": "$sub", - "content": "generates", + "content": "proposes", "fontFamily": "$font-sans", "fontSize": 11, "fontWeight": "normal" @@ -1372,8 +1279,8 @@ { "type": "text", "id": "FtMaq", - "x": 496, - "y": 403, + "x": 516, + "y": 419, "name": "cl", "fill": "$sub", "content": "answers", @@ -1385,7 +1292,7 @@ "type": "text", "id": "SEQRg", "x": 842, - "y": 458, + "y": 435, "name": "cl", "fill": "$sub", "content": "applies", @@ -1397,7 +1304,7 @@ "type": "icon", "id": "X7ZZz", "x": 677, - "y": 133, + "y": 153, "name": "flow", "width": 18, "height": 18, @@ -1409,7 +1316,7 @@ "type": "icon", "id": "E7R4B", "x": 788, - "y": 133, + "y": 153, "name": "flow", "width": 18, "height": 18, @@ -1420,8 +1327,8 @@ { "type": "icon", "id": "cU13W", - "x": 588, - "y": 320, + "x": 589, + "y": 337, "name": "flow", "width": 18, "height": 18, @@ -1432,8 +1339,8 @@ { "type": "icon", "id": "nPvS5", - "x": 457, - "y": 401, + "x": 417, + "y": 417, "name": "flow", "width": 18, "height": 18, @@ -1444,8 +1351,8 @@ { "type": "icon", "id": "tPP0L", - "x": 706, - "y": 401, + "x": 678, + "y": 417, "name": "flow", "width": 18, "height": 18, @@ -1456,8 +1363,8 @@ { "type": "icon", "id": "MuTKC", - "x": 478, - "y": 401, + "x": 496, + "y": 417, "name": "flow", "width": 18, "height": 18, @@ -1468,8 +1375,8 @@ { "type": "icon", "id": "uZiMw", - "x": 852, - "y": 440, + "x": 853, + "y": 417, "name": "flow", "width": 18, "height": 18, @@ -1480,8 +1387,8 @@ { "type": "icon", "id": "dFzPW", - "x": 994, - "y": 320, + "x": 999, + "y": 337, "name": "flow", "width": 18, "height": 18, @@ -1492,8 +1399,8 @@ { "type": "icon", "id": "sQ0Tp", - "x": 994, - "y": 401, + "x": 999, + "y": 417, "name": "flow", "width": 18, "height": 18, @@ -1504,8 +1411,8 @@ { "type": "icon", "id": "n8T0qT", - "x": 1146, - "y": 361, + "x": 1145, + "y": 417, "name": "flow", "width": 18, "height": 18, @@ -1517,31 +1424,31 @@ "type": "icon", "id": "uYXUa", "x": 179, - "y": 412, + "y": 345, "name": "flow", "width": 18, "height": 18, - "icon": "arrow-up", + "icon": "arrow-down", "library": "lucide", "fill": "$sub" }, { "type": "text", "id": "m2iJ8", - "x": 200, - "y": 415, + "x": 199, + "y": 347, "name": "cl", - "fill": "$faint", - "content": "syncs", + "fill": "$sub", + "content": "pulls", "fontFamily": "$font-sans", "fontSize": 11, - "fontWeight": "600" + "fontWeight": "normal" }, { "type": "icon", "id": "KiP80", "x": 589, - "y": 503, + "y": 497, "name": "flow", "width": 18, "height": 18, @@ -1549,96 +1456,11 @@ "library": "lucide", "fill": "$accent" }, - { - "type": "frame", - "id": "BLtmZ", - "x": 1170, - "y": 436, - "name": "Z:GENERATORS", - "width": 260, - "height": 124, - "fill": "$card2", - "cornerRadius": 14, - "stroke": "$border", - "strokeWidth": 1, - "layout": "none", - "children": [ - { - "type": "rectangle", - "cornerRadius": 2, - "id": "uNf0f", - "x": 18, - "y": 18, - "name": "dot", - "fill": "$accent", - "width": 6, - "height": 6 - }, - { - "type": "text", - "id": "Fj2md", - "x": 32, - "y": 12, - "name": "lbl", - "fill": "$sub", - "content": "GENERATORS", - "fontFamily": "$font-mono", - "fontSize": 11.5, - "fontWeight": "600", - "letterSpacing": 1 - } - ] - }, - { - "type": "frame", - "id": "mEXuA", - "x": 1190, - "y": 477, - "name": "B:View Generators", - "width": 220, - "height": 56, - "fill": "$card", - "cornerRadius": 9, - "stroke": "$border", - "strokeWidth": 1, - "layout": "vertical", - "gap": 3, - "padding": [ - 9, - 12 - ], - "justifyContent": "center", - "children": [ - { - "type": "text", - "id": "MGTMU", - "name": "t", - "fill": "$ink", - "content": "View Generators", - "fontFamily": "$font-sans", - "fontSize": 14, - "fontWeight": "700" - }, - { - "type": "text", - "id": "FNu1B", - "name": "s", - "fill": "$sub", - "textGrowth": "fixed-width", - "width": "fill_container", - "content": "view skills + deterministic pipeline", - "lineHeight": 1.25, - "fontFamily": "$font-sans", - "fontSize": 11.5, - "fontWeight": "normal" - } - ] - }, { "type": "icon", "id": "qe0FQ", - "x": 1280, - "y": 412, + "x": 1291, + "y": 497, "name": "flow", "width": 18, "height": 18, @@ -1649,8 +1471,8 @@ { "type": "text", "id": "VuPhG", - "x": 1299, - "y": 414, + "x": 1311, + "y": 499, "name": "cl", "fill": "$sub", "content": "generates", @@ -1662,7 +1484,7 @@ "type": "frame", "id": "XOSEL", "x": 1350, - "y": 487, + "y": 528, "name": "pill", "fill": "$pill", "cornerRadius": 3, @@ -1688,7 +1510,7 @@ "type": "icon", "id": "ICQx5", "x": 325, - "y": 302, + "y": 417, "name": "flow", "width": 18, "height": 18, @@ -1699,8 +1521,8 @@ { "type": "text", "id": "UQ5Xj", - "x": 313, - "y": 318, + "x": 311, + "y": 435, "name": "cl", "fill": "$sub", "content": "analyzes", @@ -1708,12 +1530,48 @@ "fontSize": 11, "fontWeight": "normal" }, + { + "type": "text", + "id": "PvCqx", + "x": 609, + "y": 499, + "name": "cl", + "fill": "$sub", + "content": "commits", + "fontFamily": "$font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fZikz", + "x": 1019, + "y": 499, + "name": "cl", + "fill": "$sub", + "content": "snapshots", + "fontFamily": "$font-sans", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "TCULu", + "x": 999, + "y": 497, + "name": "flow", + "width": 18, + "height": 18, + "icon": "arrow-down", + "library": "lucide", + "fill": "$accent" + }, { "type": "frame", - "id": "y1tnM", - "x": 610, - "y": 566, - "name": "B:Snapshot", + "id": "Z6Ks5", + "x": 78, + "y": 518, + "name": "B:Reactor", "width": 220, "height": 56, "fill": "$card", @@ -1730,22 +1588,22 @@ "children": [ { "type": "text", - "id": "F9zdsP", + "id": "O0um7", "name": "t", "fill": "$ink", - "content": "Snapshot", + "content": "Reactor", "fontFamily": "$font-sans", "fontSize": 14, "fontWeight": "700" }, { "type": "text", - "id": "qqAdB", + "id": "a5e27", "name": "s", "fill": "$sub", "textGrowth": "fixed-width", "width": "fill_container", - "content": "the graph as model.json", + "content": "re-syncs on change, opt-in", "lineHeight": 1.25, "fontFamily": "$font-sans", "fontSize": 11.5, @@ -1754,138 +1612,133 @@ ] }, { - "type": "text", - "id": "HQAlC", - "x": 872, - "y": 594, - "name": "cl", - "fill": "$sub", - "content": "sync", - "fontFamily": "$font-sans", - "fontSize": 11, - "fontWeight": "normal" - }, - { - "type": "text", - "id": "AIpWG", - "x": 943, - "y": 594, - "name": "cl", - "fill": "$sub", - "content": "run", - "fontFamily": "$font-sans", - "fontSize": 11, - "fontWeight": "normal" - }, - { - "type": "text", - "id": "ZZuhq", - "x": 1002, - "y": 594, - "name": "cl", - "fill": "$sub", - "content": "answer", - "fontFamily": "$font-sans", - "fontSize": 11, - "fontWeight": "normal" - }, - { - "type": "text", - "id": "dLXLF", - "x": 1074, - "y": 594, - "name": "cl", - "fill": "$sub", - "content": "apply", - "fontFamily": "$font-sans", - "fontSize": 11, - "fontWeight": "normal" - }, - { - "type": "rectangle", - "id": "VOSEm", - "x": 869, - "y": 585, - "name": "commit trunk", - "fill": "#aaa090", - "width": 237, - "height": 1 - }, - { - "type": "ellipse", - "id": "q5f0Y", - "x": 1083, - "y": 581, - "name": "commit dot", - "fill": "$accent", - "width": 10, - "height": 10 - }, - { - "type": "ellipse", - "id": "ZvYZQ", - "x": 1015, - "y": 581, - "name": "commit dot", - "fill": "$accent", - "width": 10, - "height": 10 - }, - { - "type": "ellipse", - "id": "x3pdh", - "x": 947, - "y": 581, - "name": "commit dot", - "fill": "$accent", - "width": 10, - "height": 10 - }, - { - "type": "ellipse", - "id": "gl3he", - "x": 879, - "y": 581, - "name": "commit dot", - "fill": "$accent", - "width": 10, - "height": 10 - }, - { - "type": "text", - "id": "PvCqx", - "x": 606, - "y": 505, - "name": "cl", - "fill": "$sub", - "content": "commits", - "fontFamily": "$font-sans", - "fontSize": 11, - "fontWeight": "normal" - }, - { - "type": "text", - "id": "fZikz", - "x": 1017, - "y": 505, - "name": "cl", - "fill": "$sub", - "content": "snapshots", - "fontFamily": "$font-sans", - "fontSize": 11, - "fontWeight": "normal" + "type": "frame", + "id": "w6VZS", + "x": 1190, + "y": 278, + "name": "B:MCP", + "width": 220, + "height": 56, + "fill": "$card", + "cornerRadius": 9, + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 3, + "padding": [ + 9, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "Y3CkVY", + "name": "t", + "fill": "$ink", + "content": "MCP", + "fontFamily": "$font-sans", + "fontSize": 14, + "fontWeight": "700" + }, + { + "type": "text", + "id": "unEIT", + "name": "s", + "fill": "$sub", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "read-only tools over the graph", + "lineHeight": 1.25, + "fontFamily": "$font-sans", + "fontSize": 11.5, + "fontWeight": "normal" + } + ] }, { - "type": "icon", - "id": "TCULu", - "x": 999, - "y": 503, - "name": "flow", - "width": 18, - "height": 18, - "icon": "arrow-down", - "library": "lucide", - "fill": "$accent" + "type": "frame", + "id": "E5OHYg", + "x": 78, + "y": 373, + "name": "B:Intent + Code", + "width": 220, + "fill": "$card", + "cornerRadius": 9, + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 14, + "padding": 12, + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "t1176J", + "name": "Intent", + "width": "fill_container", + "layout": "vertical", + "gap": 3, + "children": [ + { + "type": "text", + "id": "xT5Qo", + "name": "t", + "fill": "$ink", + "content": "Intent", + "fontFamily": "$font-sans", + "fontSize": 14, + "fontWeight": "700" + }, + { + "type": "text", + "id": "oDNyE", + "name": "s", + "fill": "$sub", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "PRDs, issues", + "lineHeight": 1.25, + "fontFamily": "$font-sans", + "fontSize": 11.5, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "fWwx3", + "name": "Code", + "width": "fill_container", + "layout": "vertical", + "gap": 3, + "children": [ + { + "type": "text", + "id": "F5p9Fm", + "name": "t", + "fill": "$ink", + "content": "Code", + "fontFamily": "$font-sans", + "fontSize": 14, + "fontWeight": "700" + }, + { + "type": "text", + "id": "LpZx0", + "name": "s", + "fill": "$sub", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "repositories, microservices", + "lineHeight": 1.25, + "fontFamily": "$font-sans", + "fontSize": 11.5, + "fontWeight": "normal" + } + ] + } + ] } ] } diff --git a/.github/assets/architecture.png b/.github/assets/architecture.png new file mode 100644 index 00000000..bc6f3306 Binary files /dev/null and b/.github/assets/architecture.png differ diff --git a/.github/assets/graph-and-ask.png b/.github/assets/graph-and-ask.png new file mode 100644 index 00000000..63b02d24 Binary files /dev/null and b/.github/assets/graph-and-ask.png differ diff --git a/.github/assets/graph-and-ask.webp b/.github/assets/graph-and-ask.webp new file mode 100644 index 00000000..cfc6b9b6 Binary files /dev/null and b/.github/assets/graph-and-ask.webp differ diff --git a/.github/assets/reference-and-tutorial.png b/.github/assets/reference-and-tutorial.png new file mode 100644 index 00000000..d66c26c1 Binary files /dev/null and b/.github/assets/reference-and-tutorial.png differ diff --git a/.github/assets/reference-and-tutorial.webp b/.github/assets/reference-and-tutorial.webp new file mode 100644 index 00000000..4bd2dc3c Binary files /dev/null and b/.github/assets/reference-and-tutorial.webp differ diff --git a/.github/assets/review-loop.webp b/.github/assets/review-loop.webp new file mode 100644 index 00000000..f7a8abdc Binary files /dev/null and b/.github/assets/review-loop.webp differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93670590..8e3be464 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm lint @@ -37,7 +37,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm typecheck @@ -48,7 +48,7 @@ jobs: strategy: fail-fast: false matrix: - node: ['20', '22'] + node: ['22', '24'] steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 @@ -70,7 +70,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1cda230c..8a6dae35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: pnpm registry-url: 'https://registry.npmjs.org' diff --git a/Dockerfile b/Dockerfile index 25080802..1e8e9c1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ # --------------------------------------------------------------------------- # deps: install the workspace and build the UI # --------------------------------------------------------------------------- -FROM node:20-slim AS deps +FROM node:22-slim AS deps ENV PNPM_HOME=/pnpm ENV PATH=$PNPM_HOME:$PATH # pnpm asks before clearing a modules directory, and there is no TTY to answer. @@ -39,7 +39,7 @@ RUN pnpm --filter @braidhq/studio build # --------------------------------------------------------------------------- # uv: fetch uvx here so curl never enters the runtime # --------------------------------------------------------------------------- -FROM node:20-slim AS uv +FROM node:22-slim AS uv RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates \ && rm -rf /var/lib/apt/lists/* @@ -48,7 +48,7 @@ RUN curl -LsSf https://astral.sh/uv/install.sh | sh # --------------------------------------------------------------------------- # runtime # --------------------------------------------------------------------------- -FROM node:20-slim AS runtime +FROM node:22-slim AS runtime ENV PNPM_HOME=/pnpm ENV PATH=$PNPM_HOME:$PATH RUN corepack enable diff --git a/README.md b/README.md index c3798d3a..3f6a1296 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,38 @@ # Braid [![CI](https://github.com/mroops0111/braid/actions/workflows/ci.yml/badge.svg)](https://github.com/mroops0111/braid/actions/workflows/ci.yml) +[![npm version](https://img.shields.io/npm/v/@braidhq/cli.svg)](https://www.npmjs.com/package/@braidhq/cli) +[![npm downloads](https://img.shields.io/npm/dm/@braidhq/cli.svg)](https://www.npmjs.com/package/@braidhq/cli) +[![Node Version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmroops0111%2Fbraid%2Fmaster%2Fpackage.json&query=%24.engines.node&label=node&color=339933&logo=node.js&logoColor=white)](https://nodejs.org) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![Node](https://img.shields.io/badge/node-%3E%3D20-339933.svg?logo=node.js&logoColor=white)](https://nodejs.org) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178C6.svg?logo=typescript&logoColor=white)](https://www.typescriptlang.org) -_A harness framework that keeps AI and your team building one domain model together, in a loop where the AI drafts and asks, and people decide._ +_A framework for one reviewed model of your domain. Agents draft it from your sources, people decide what lands, and every node cites what it was drawn from._ -![Braid Studio: answering a clarification, running clarify, then reviewing and applying the proposal to the domain model](demo.gif) +![Braid Studio: running extract over one issue, reading the evidence and the operations behind a proposal, then applying it](.github/assets/review-loop.webp) + +_A finished model: [`examples/conciergent`](examples/conciergent/), 120 nodes Braid drew from one codebase with no intent docs._ **A shared model of your business, not another code graph.** Code is what shipped. Intent is what the team meant. They drift apart every sprint, and the team ends up arguing about which one is right. Braid _braids_ them back into one domain model that engineers and PMs can both read. The default ontology is Domain-Driven Design (DDD), so people and the AI both speak the ubiquitous language of the domain instead of class names and package paths. +Braid harnesses a coding agent, Claude Code. Each run sees only the tools its kind of run may call, and a node that cites nothing is refused. The harness is what lets an AI be the author here. + ## Features -- **Human-in-the-Loop Gate**: the AI drafts and asks, but a person decides before any change lands. -- **Evidence-Backed Claims**: every node traces back to the file or document it was drawn from. -- **Docs That Never Drift**: a document is a projection of the one model, regenerated on demand rather than kept in sync by hand. -- **Continuous Reaction**: as sources change, Braid feeds the diff back as a fresh Proposal instead of going stale. -- **Git-Versioned History**: every human-gated write commits to Git, so any point in the model's history is restorable. +- **Human-in-the-Loop Gate**: the AI drafts and asks, but a person decides before any change lands, and every decision it lands commits to Git. +- **Cross-Source Comparison**: intent and code are read against each other, each recorded with the lines it came from. Where the two disagree, Braid records it with both references and leaves which one is right to a person. +- **Rendered Output**: what a run produces arrives as the shapes that suit it, a table, a matrix, a finding, a piece of the graph. The model picks which shapes it needs. How they look belongs to the UI, so every run reads the same way. +- **Projected Views**: one model, and an artifact for each reason to read it, a reference or a tutorial, each saying when it falls behind. Not one PRD with one viewpoint, stale for everyone at once. +- **Continuous Reaction**: as sources change, Braid feeds the diff back as a fresh Proposal, so the canonical graph keeps up instead of becoming a snapshot of the day it was built. ## Motivation -Braid is built against two failure modes. +Braid is built against three failure modes. - **Code-Only Graphs**: tools that pull a graph straight from source are honest about what runs, but the result is a class-and-call-site graph. It cannot tell you why a feature exists, who asked for it, or what trade-off shaped its rules. PMs cannot read it. -- **Doc-Only Knowledge**: PRDs, design docs, Notion, and Confluence speak the domain, but nobody keeps them in sync once the code lands. Several months later, nobody trusts them. +- **Doc-Only Knowledge**: PRDs, design docs, Notion, and Confluence speak the domain, but nothing ties a sentence to the code behind it. Once the code moves nobody can say which parts still hold, so the whole set stops being trusted. +- **Folder-Only Structure**: nesting is the only relationship a set of documents has. One document is a workflow, the next a state machine, the next a rule that constrains half the product, and nothing can record that the third governs the other two. ## Design @@ -38,20 +44,53 @@ Every axis is a plugin, and the defaults are a starting point rather than a buil - **Swappable Axes**: the ontology, source loaders, storage, agent, and view generators are all plugins, each overridable in one manifest field. - **Framework Invariants**: the human-in-the-loop gate, the evidence requirement, and the branded type discipline are enforced by the type system and cannot be swapped out. -- **Braid Anything**: the domain lives in the ontology, not the engine, so the same loop, gate, and provenance carry over whether you braid a codebase, a research corpus, or a product spec. +- **Braid Anything**: the same loop, gate, and provenance carry over whether you braid a codebase, a research corpus, or a product spec. + +### The Ontology + +Of the five axes this is the one a workspace feels, because it decides what the graph is made of and in whose words. Four things every ontology declares, answered below by the DDD one a workspace gets unless it names another. + +- **Types and Named Edges**: what a node may be, and which relationships may hold between them. DDD ships eight types and fifteen edges, so a rule constraining an operation is recorded as that edge, and the graph can be asked which rules reach it. +- **Source Roles**: what a workspace must supply. DDD requires code and takes intent wherever there is any. +- **Audiences**: who an answer is written for, and how much of a reference each one sees. DDD splits business from engineering. +- **Build Skills**: the pipeline itself. Build reads the steps, their order, and their labels from here, so swapping the ontology changes the pipeline and nothing else. ### Architecture The server is the composition root. Sources feed an event-driven engine that produces reviewable changes, a human gate lands them in one canonical model, and every write is versioned in Git. -![Braid architecture](architecture.png) +![Braid architecture](.github/assets/architecture.png) + +- **Surfaces**: Studio (web UI), Desktop, and MCP clients all talk to one server, over REST with an SSE event stream and AG-UI beside it. The CLI scaffolds a workspace and boots the stack. +- **Sources**: Intent and Code are pulled into the workspace by Source Loader plugins, and the Reactor re-syncs them as they change, wherever a workspace opts in. +- **Engine**: the Agent is a plugin like the rest, and the Skills it runs each see only their own tools. Where a run reaches a point only a person can settle it emits a Handoff, either a Proposal or a Clarification, and both land in one queue, because what makes them one kind is who must act next. What a person lands commits to the versioned workspace tree. +- **Model**: the Ontology types the graph, the graph is the single source of truth, a Storage plugin such as Kuzu persists it, and a `model.json` snapshot travels with the code. +- **Reads**: read-only MCP tools over the graph, Ask for a question a run answers, and Views projected by a generator, one artifact per reason to read. + +### AI-Native Design + +The AI is the author and a person is the reviewer, which is the reverse of the usual arrangement. Everything below follows from that one inversion. + +- **No Authoring Surface**: there is no form for creating a node. The graph grows only when a run proposes and a person applies, and every node carries the source it was drawn from. Both are invariants, not settings. +- **Composed at Run Time**: what a run does is not written in code. A skill supplies the instruction, and the tools it may call are handed to it as a set narrowed to that kind of run, so an `ask` run does not decline to propose, it cannot see the call. Change either and the behaviour changes, with nothing in the engine touched. +- **A Standard Wire**: a run streams over [AG-UI](https://github.com/ag-ui-protocol/ag-ui), a protocol Braid did not invent, so any client that speaks it can drive or display a run. Studio reads it with the protocol's own client, which is what makes that claim checkable. + +### Reading the Graph + +The graph is readable on its own, and readable through something that composes an answer out of it. + +[![Braid Studio: the command palette answering "how a chat message becomes a reply" with nodes that never use those words](.github/assets/graph-and-ask.png)](.github/assets/graph-and-ask.webp) + +_The question says message and reply. `Run Turn` and `Conversation Turn Lifecycle` say neither, and they are what it was asking for. [Play the clip](.github/assets/graph-and-ask.webp) to see one opened, then the same question put to Ask._ -- **Surfaces**: Studio (web UI), Desktop, CLI, and MCP clients all talk to one server over REST and SSE. -- **Sources**: Intent (PRDs, RFCs, issues) and Code (repositories) are pulled in by Source Loader plugins for git, github, and gdrive. -- **Engine (The HITL Loop)**: the Agent runs Skills as subprocesses. Skills analyze the sources and generate two human-in-the-loop artifacts, a Proposal (a proposed change to the model) and a Clarification (a question to resolve ambiguity). -- **Model**: an Ontology types the graph, the Graph is the single source of truth, and a Storage plugin such as Kuzu persists it. -- **Reads**: Ask answers a one-off question over the graph, and View Generators project docs and other Views off it. -- **History**: every human-gated write commits to Git. The graph state travels alongside the code as a `model.json` snapshot, so any commit is restorable. +[![Braid Studio showing the tutorial written out of one subsystem, with the reference of the same subsystem beside it in the row, both marked out of date](.github/assets/reference-and-tutorial.png)](.github/assets/reference-and-tutorial.webp) + +_One subject, two forms, both already out of date because the graph moved under them. [Play the clip](.github/assets/reference-and-tutorial.webp) to read down each of them._ + +- **The Graph Surface**: the canvas and the table, search, and a node with its evidence and its neighbours. Point an embedding endpoint at a workspace and search ranks by meaning as well as by name, so a question finds a node that never uses its words. +- **MCP**: a read-only endpoint at `/braid/mcp` carrying seven operations, from node search to the whole snapshot. It exchanges each caller's own token, so a person's client reads the graph as them. +- **Ask**: a question a run answers over the graph, composed as blocks. +- **Views**: a generator declares what it may be written about and the forms it writes, projects the subject into material, and one skill per form writes the page. ## Usage @@ -59,7 +98,9 @@ Get a workspace running, then work the review loop in Studio. ### Quick Start -Braid runs from the monorepo today. Clone it, install, and start the dev stack. +Two ways in. Both need the `claude` CLI signed in, since a skill run is a subprocess of it and nothing runs without one. + +**From a checkout**, for working on Braid itself. Node 22 or later, and pnpm. ```bash git clone https://github.com/mroops0111/braid @@ -67,15 +108,26 @@ cd braid && pnpm install && pnpm dev # Studio at http://localhost:5173, server at :4321 ``` -Open Studio and create a workspace with the Wizard, then add your intent and code sources. The default ontology is DDD. +**From the container**, which is what a deployment runs. One image serves the API and the built Studio on one origin, so there is no Vite in the picture. + +```bash +cp .env.example .env # fill in the four required values +docker compose up +# Studio and the API at http://localhost:4321 +``` + +Either way, open Studio and create a workspace with the Wizard, then add your intent and code sources. + +`.env.example` names what a deployment sets and what each value turns on. `compose.oidc.yaml` adds Keycloak for a deployment that wants its own authorization server, which is also what turns the MCP endpoint on. [`@braidhq/server`](packages/server/README.md#deployment) documents every variable the server reads and what each does when absent. ### The Loop -Once the dev server is running, work the loop in Studio at `http://localhost:5173`. +Four steps, and a person ends every one of them. -- **Extract**: run a skill such as `/ddd:extract` from the Actions tab. -- **Review**: open each Proposal, inspect the diff, and pre-validate it. Answer any Clarification the agent raised. -- **Apply**: land the change when it is green, or reject it with a reason. +- **Build**: run the ontology's pipeline over one source document or over a group. Build lists every document and what the model has made of it so far. +- **Review**: read what the run left in the Inbox, where a question it stopped on and a change it proposed sit as two kinds of card in one queue. +- **Apply**: land the change when its validation is green, or reject it with a reason. The queue moves to the next card either way. +- **Read**: put a question to Ask, or write a subject into a document on Documents and pick the form it takes. ## Packages @@ -91,29 +143,37 @@ Once the dev server is running, work the loop in Studio at `http://localhost:517 | Package | Description | |---|---| -| [`@braidhq/ontology-ddd`](packages/ontology-ddd/) | Default DDD ontology: boundedContext, aggregate, command, query, event, rule, and actor. | +| [`@braidhq/ontology-ddd`](packages/ontology-ddd/) | Default DDD ontology: boundedContext, aggregate, command, query, event, rule, actor, and policy. | | [`@braidhq/storage-kuzu`](packages/storage-kuzu/) | Embedded Kuzu graph store, a zero-infra single-binary alternative to Neo4j. | | [`@braidhq/source-loader-git`](packages/source-loader-git/) | Clone a repository and sync it automatically. | | [`@braidhq/source-loader-github`](packages/source-loader-github/) | Sync a GitHub repository over the API, OAuth on first use. | | [`@braidhq/source-loader-gdrive`](packages/source-loader-gdrive/) | Export documents from Google Drive, OAuth on first use. | +| [`@braidhq/source-loader-mcp`](packages/source-loader-mcp/) | Mirror an API-backed source by paging one MCP tool, one file per item. | +| [`@braidhq/view-generator-doc`](packages/view-generator-doc/) | Project a container node into material, and write it as a `reference` or a `tutorial`. | | [`@braidhq/agent-claude-code`](packages/agent-claude-code/) | Runs the `claude` CLI to execute SKILL.md prompts, the default LLM backend. | **Surfaces** | Package | Description | |---|---| -| [`@braidhq/server`](packages/server/) | REST and SSE server, the composition root. | +| [`@braidhq/server`](packages/server/) | REST, SSE, and AG-UI server, the composition root. | | [`@braidhq/cli`](packages/cli/) | Command-line entry point. | | [`@braidhq/studio`](packages/studio/) | Web UI. | | [`@braidhq/desktop`](packages/desktop/) | Tauri desktop shell. | ## Extending Braid -Braid has two extension surfaces, and neither touches the core. A TypeScript plugin adds a swappable axis. A Markdown skill adds an AI capability. +Two extension surfaces, neither touching the core. Both are files on disk, not forms in Studio. + +**A plugin** adds a swappable axis. It implements a port, and you register it where you start the server, `startServer({ extraOntologyPlugins: [mine] })`, so swapping an axis means owning the entry point rather than configuring `braid serve`. See [`@braidhq/sdk`](packages/sdk/) for the five `define*Plugin` builders, and the shipped plugins for implementations to copy. -A plugin implements a port and registers at server start-up, then a workspace opts in by name. See [`@braidhq/sdk`](packages/sdk/) for the ontology and source-loader builders. The shipped plugins, such as [`@braidhq/storage-kuzu`](packages/storage-kuzu/) and [`@braidhq/source-loader-git`](packages/source-loader-git/), are reference implementations to copy from. +**A skill** adds an AI capability. It is a `SKILL.md` at `/skills//SKILL.md`, invoked as `/workspace:`, and its `category` decides where it belongs. -A skill is a `SKILL.md` file at `/skills//SKILL.md`, invoked as `/workspace:`. It shows up in Studio's Actions tab alongside the built-ins on the next request. +- **`build`**: its own stage on Build, running over a source document, and may propose. One run at a time. The one category a workspace can add by itself. +- **`ask`**: answers on Ask, offered the render calls but no way to write. Ask reaches for one such skill, today the built-in one, so a second is read and not yet offered. +- **`generate`**: writes a document, and the only category offered `showSection`, `showCheck`, and `showCustom`. A form and its writer are declared together by a view generator, so this one arrives with a plugin. + +An `EXTEND.md` at `/skill-extensions//` adds rules to a built-in skill instead of replacing it. ```markdown --- @@ -122,13 +182,18 @@ description: Extract DDD entities from intent + code argumentHint: model: opus braid: + category: build requiredEnv: [GITHUB_TOKEN] --- -Walk `intent/` and `code/`. Emit proposals that add boundedContext, aggregate, and command nodes. Cite the source file or doc each claim came from. +Walk `intent/` and `code/`. Emit proposals that add boundedContext, aggregate, and command nodes. Cite the source and the lines each node was drawn from. ``` The `braid:` block is preflighted before the agent spawns, so a missing env var or MCP server fails fast with a clear error. +## Status + +Braid is at `0.6.0` and pre-1.0. It is used against real workspaces, and the shape of the graph, the review loop, and the human gate have held for months. The surfaces and the plugin contracts still move between minor versions, and a release that breaks one says so. + ## License [MIT](LICENSE) diff --git a/architecture.png b/architecture.png deleted file mode 100644 index fc923a36..00000000 Binary files a/architecture.png and /dev/null differ diff --git a/compose.oidc.yaml b/compose.oidc.yaml new file mode 100644 index 00000000..af17b911 --- /dev/null +++ b/compose.oidc.yaml @@ -0,0 +1,101 @@ +# Braid behind your own authorization server, with Keycloak as the example. +# +# docker compose -f compose.yaml -f compose.oidc.yaml up +# +# Braid depends on no particular one. It reads BRAID_OIDC_ISSUER and speaks +# OIDC, so Auth0, Okta, Entra, or Zitadel substitute for the service below. +# +# An issuer does two things. Both doors move onto it, displacing the Google +# client, and the read-only MCP endpoint turns on. +# +# Google still signs everybody in, from behind Keycloak rather than beside +# it. Braid offers one identity provider, so putting Keycloak in front would +# otherwise take Google away from the people already using it. + +services: + keycloak: + image: quay.io/keycloak/keycloak:26.4 + command: + - start-dev + environment: + # Bootstrap only. Change the password after the first sign-in. + KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_BOOTSTRAP_ADMIN_USERNAME:-admin} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_BOOTSTRAP_ADMIN_PASSWORD:?set an admin password} + # The issuer every token is checked against. + # It has to be the name Braid and its MCP callers both use. + KC_HOSTNAME: ${KEYCLOAK_URL:?set this to the URL Keycloak answers on} + # Serves /health/ready on the management port, which the check reads. + KC_HEALTH_ENABLED: 'true' + ports: + - '8080:8080' + volumes: + # Users federated from Google live here. + # Losing it locks nobody out, since Braid falls back to the email, + # but every returning person would look new. + - keycloak-data:/opt/keycloak/data + restart: unless-stopped + healthcheck: + # The image ships no curl, so this is Keycloak's own bash workaround. + test: >- + exec 3<>/dev/tcp/127.0.0.1/9000 + && echo -e 'GET /health/ready HTTP/1.1\nhost: localhost\n' >&3 + && cat <&3 | grep -q '200 OK' + interval: 30s + timeout: 5s + start_period: 60s + retries: 5 + + # Runs once per deploy and exits. + # Idempotent, so it re-asserts the realm without disturbing anyone. + keycloak-init: + image: quay.io/keycloak/keycloak:26.4 + depends_on: + keycloak: + condition: service_started + entrypoint: + - sh + - /init.sh + environment: + KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_BOOTSTRAP_ADMIN_USERNAME:-admin} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_BOOTSTRAP_ADMIN_PASSWORD:?} + BRAID_API_URL: ${BRAID_STUDIO_URL} + BRAID_OIDC_CLIENT_ID: ${BRAID_OIDC_CLIENT_ID:?} + BRAID_OIDC_CLIENT_SECRET: ${BRAID_OIDC_CLIENT_SECRET:?} + BRAID_GOOGLE_CLIENT_ID: ${BRAID_GOOGLE_CLIENT_ID:?} + BRAID_GOOGLE_CLIENT_SECRET: ${BRAID_GOOGLE_CLIENT_SECRET:?} + # The two clients the script creates alongside Braid's own. + # One is an audience the exchanged token can carry, + # the other is what the MCP gateway authenticates as. + KEYCLOAK_API_SECRET: ${KEYCLOAK_API_SECRET:?} + KEYCLOAK_GATEWAY_SECRET: ${KEYCLOAK_GATEWAY_SECRET:?} + # Narrows Google to one workspace domain. Empty lets any account in. + GOOGLE_HOSTED_DOMAIN: ${GOOGLE_HOSTED_DOMAIN:-} + volumes: + - ./keycloak/init.sh:/init.sh:ro + # eslint-disable-next-line yaml/plain-scalar -- unquoted `no` parses as false, which compose rejects + restart: 'no' + + braid: + depends_on: + keycloak-init: + condition: service_completed_successfully + environment: + # server: the realm url, and Braid's own client for the browser sign-in. + BRAID_OIDC_ISSUER: ${BRAID_OIDC_ISSUER:?the realm url, not the server root} + BRAID_OIDC_CLIENT_ID: ${BRAID_OIDC_CLIENT_ID:?} + BRAID_OIDC_CLIENT_SECRET: ${BRAID_OIDC_CLIENT_SECRET:?} + # server: the MCP endpoint's own client, used to exchange a caller's + # token (RFC 8693) for one whose audience names this API. + BRAID_MCP_GATEWAY_CLIENT_ID: ${BRAID_API_URL:-${BRAID_STUDIO_URL}}/braid/mcp + BRAID_MCP_GATEWAY_CLIENT_SECRET: ${KEYCLOAK_GATEWAY_SECRET:?} + networks: + default: + # The gateway inside Braid reaches the issuer by its public name, + # since that name is what every token is checked against. + # A host does not route a container back to its own public address, + # so the alias resolves it on this network instead. + aliases: + - ${KEYCLOAK_HOSTNAME:-keycloak} + +volumes: + keycloak-data: diff --git a/compose.yaml b/compose.yaml index f32ef699..48dcf9da 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,29 +4,62 @@ services: ports: - '4321:4321' volumes: - # Everything that must survive a redeploy. Without it every source - # re-clones on each boot, and the graph and its git history are lost. + # Everything that must survive a redeploy. + # Without it every source re-clones, and the graph and its history go. - braid-data:/data + # What one deployment differs from another by. + # Copy .env.example to .env and fill it in, Compose reads it from here. environment: - # Real authentication. Leaving this true trusts every caller. + # ------------------------------------------------------------------- + # required + # ------------------------------------------------------------------- + # server: real authentication. Left true, every caller is trusted. BRAID_LOCAL_TRUST: 'false' - # Where users reach Studio. Also the only browser origin trusted by - # default, so a wrong value here is what a CORS failure looks like. + # server: where users reach Studio, and the one origin trusted by default. + # A wrong value here is what a CORS failure looks like. BRAID_STUDIO_URL: ${BRAID_STUDIO_URL:?set this to the URL users open} - # This server serves Studio, so the API answers on that same origin. - # Both OAuth redirects derive from it, and each has to be registered - # in the Google console exactly as derived. + # server: the same origin, since this process serves Studio. + # Both Google redirects derive from it, so each must match the console. BRAID_API_URL: ${BRAID_STUDIO_URL} + # server: who may create workspaces and manage the roster. + BRAID_ADMIN_EMAILS: ${BRAID_ADMIN_EMAILS:?at least one admin is required} + # server signs people in with these. + # source-loader-gdrive connects a Drive source with them too. BRAID_GOOGLE_CLIENT_ID: ${BRAID_GOOGLE_CLIENT_ID:?} BRAID_GOOGLE_CLIENT_SECRET: ${BRAID_GOOGLE_CLIENT_SECRET:?} - BRAID_ALLOWED_DOMAINS: ${BRAID_ALLOWED_DOMAINS:-} - BRAID_ADMIN_EMAILS: ${BRAID_ADMIN_EMAILS:?at least one admin is required} - # Subscription credential for the agent. See the deployment notes in - # packages/server/README.md for how it is minted and what it costs. + # agent-claude-code: the seat every run draws on. CLAUDE_CODE_OAUTH_TOKEN: ${CLAUDE_CODE_OAUTH_TOKEN:?} - # Read access for private git remotes, referenced from PRODUCT.md as - # `https://oauth2:${GIT_TOKEN}@host/group/repo.git`. + + # ------------------------------------------------------------------- + # optional + # ------------------------------------------------------------------- + # An empty value leaves each of these off. + # server: restricts sign-in to these domains, comma separated. + BRAID_ALLOWED_DOMAINS: ${BRAID_ALLOWED_DOMAINS:-} + # source-loader-git: read access for a private remote. GIT_TOKEN: ${GIT_TOKEN:-} + # source-loader-github: an Issues source, connected per source. + BRAID_GITHUB_CLIENT_ID: ${BRAID_GITHUB_CLIENT_ID:-} + BRAID_GITHUB_CLIENT_SECRET: ${BRAID_GITHUB_CLIENT_SECRET:-} + # server: sign-in through your own authorization server. + # An issuer displaces the Google client and turns on the MCP endpoint. + BRAID_OIDC_ISSUER: ${BRAID_OIDC_ISSUER:-} + BRAID_OIDC_CLIENT_ID: ${BRAID_OIDC_CLIENT_ID:-} + BRAID_OIDC_CLIENT_SECRET: ${BRAID_OIDC_CLIENT_SECRET:-} + # BRAID_OIDC_AUDIENCE is absent on purpose. + # It defaults to the API url, and an empty value would not read as unset. + # server: the MCP endpoint's own client at that issuer. + BRAID_MCP_GATEWAY_CLIENT_ID: ${BRAID_MCP_GATEWAY_CLIENT_ID:-} + BRAID_MCP_GATEWAY_CLIENT_SECRET: ${BRAID_MCP_GATEWAY_CLIENT_SECRET:-} + # server: lets each person save their own agent credential. + # Without it none can, and every run spends the shared seat. + BRAID_SECRET_KEY: ${BRAID_SECRET_KEY:-} + BRAID_SECRET_KEY_PREVIOUS: ${BRAID_SECRET_KEY_PREVIOUS:-} + # server: search by meaning. The address is the switch. + # Any OpenAI-compatible endpoint, Ollama included. + BRAID_EMBEDDING_HOST: ${BRAID_EMBEDDING_HOST:-} + BRAID_EMBEDDING_MODEL: ${BRAID_EMBEDDING_MODEL:-} + BRAID_EMBEDDING_API_KEY: ${BRAID_EMBEDDING_API_KEY:-} restart: unless-stopped volumes: diff --git a/demo.gif b/demo.gif deleted file mode 100644 index 57ad4879..00000000 Binary files a/demo.gif and /dev/null differ diff --git a/keycloak/init.sh b/keycloak/init.sh new file mode 100755 index 00000000..9a8bd5e5 --- /dev/null +++ b/keycloak/init.sh @@ -0,0 +1,161 @@ +#!/bin/sh +# Provisions the realm Braid signs people in against. +# Idempotent, so a redeploy re-asserts the shape without disturbing users. +# Every secret arrives by environment, so nothing here holds one. +# Google sits behind Keycloak, not beside it, since Braid offers one provider. +set -eu + +KC=/opt/keycloak/bin/kcadm.sh +# Over the compose network, so provisioning never waits on the public name. +SERVER=${KEYCLOAK_INTERNAL_URL:-http://keycloak:8080} +REALM=${KEYCLOAK_REALM:-braid} + +# Keycloak resolves a requested audience to a client by its id, +# so the API's own URL has to name a client. +API_CLIENT="$BRAID_API_URL" +MCP_CLIENT="$BRAID_API_URL/braid/mcp" + +echo "Waiting for Keycloak..." +until $KC config credentials --server "$SERVER" --realm master \ + --user "$KC_BOOTSTRAP_ADMIN_USERNAME" --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null 2>&1; do + sleep 3 +done +echo "Keycloak is ready." + +if $KC get "realms/$REALM" >/dev/null 2>&1; then + echo "Realm exists." +else + $KC create realms -s "realm=$REALM" -s enabled=true + echo "Realm created." +fi + +# Create or update, keyed on clientId, so a rerun does not duplicate. +upsert_client () { + client_id="$1" + shift + existing=$($KC get clients -r "$REALM" -q "clientId=$client_id" --fields id --format csv --noquotes 2>/dev/null | head -1) + if [ -n "$existing" ]; then + $KC update "clients/$existing" -r "$REALM" "$@" + echo "Updated client $client_id" + else + $KC create clients -r "$REALM" -s "clientId=$client_id" "$@" + echo "Created client $client_id" + fi +} + +# Studio's browser login. Confidential, so the browser holds no token. +upsert_client "$BRAID_OIDC_CLIENT_ID" \ + -s enabled=true \ + -s publicClient=false \ + -s standardFlowEnabled=true \ + -s serviceAccountsEnabled=false \ + -s "secret=$BRAID_OIDC_CLIENT_SECRET" \ + -s "redirectUris=[\"$BRAID_API_URL/auth/oidc/callback\"]" \ + -s "webOrigins=[\"$BRAID_API_URL\"]" \ + -s "attributes.\"post.logout.redirect.uris\"=$BRAID_API_URL/*" + +# Named for the API, since its only job is to be an audience a token carries. +upsert_client "$API_CLIENT" \ + -s enabled=true \ + -s publicClient=false \ + -s standardFlowEnabled=false \ + -s "secret=$KEYCLOAK_API_SECRET" + +# What the gateway authenticates as. Exchange keeps the caller themselves, +# rather than collapsing every call into one service identity. +upsert_client "$MCP_CLIENT" \ + -s enabled=true \ + -s publicClient=false \ + -s standardFlowEnabled=false \ + -s serviceAccountsEnabled=true \ + -s "secret=$KEYCLOAK_GATEWAY_SECRET" \ + -s 'attributes."standard.token.exchange.enabled"=true' + +# hostedDomain narrows Google to one workspace domain. +# Left empty, BRAID_ALLOWED_DOMAINS is the only thing narrowing it. +if $KC get identity-provider/instances/google -r "$REALM" >/dev/null 2>&1; then + $KC update identity-provider/instances/google -r "$REALM" \ + -s "config.clientId=$BRAID_GOOGLE_CLIENT_ID" \ + -s "config.clientSecret=$BRAID_GOOGLE_CLIENT_SECRET" + echo "Updated Google identity provider." +else + $KC create identity-provider/instances -r "$REALM" \ + -s alias=google -s providerId=google -s enabled=true \ + -s trustEmail=true \ + -s "config.clientId=$BRAID_GOOGLE_CLIENT_ID" \ + -s "config.clientSecret=$BRAID_GOOGLE_CLIENT_SECRET" \ + -s "config.hostedDomain=${GOOGLE_HOSTED_DOMAIN:-}" + echo "Created Google identity provider." +fi + +# The client-scopes endpoint ignores a name filter and answers with everything, +# so filtering there and taking row one returns an unrelated scope. +scope_id_of () { + $KC get client-scopes -r "$REALM" --fields id,name --format csv --noquotes 2>/dev/null \ + | grep ",$1\$" | cut -d, -f1 | head -1 +} + +# Two audiences to arrange, and neither client can ask for its own. +# An MCP client registers itself, so a realm default scope gives it one. +# The exchange then targets the API, +# and Keycloak only issues an audience the client's scope already covers. +upsert_scope () { + scope_name="$1" + audience_key="$2" + audience_value="$3" + id=$(scope_id_of "$scope_name") + if [ -z "$id" ]; then + $KC create client-scopes -r "$REALM" -s "name=$scope_name" -s protocol=openid-connect \ + -s 'attributes."include.in.token.scope"=false' + id=$(scope_id_of "$scope_name") + echo "Created client scope $scope_name" + fi + mapper=$($KC get "client-scopes/$id/protocol-mappers/models" -r "$REALM" \ + --fields id,name --format csv --noquotes 2>/dev/null \ + | grep ",$scope_name\$" | cut -d, -f1 | head -1) + if [ -n "$mapper" ]; then + $KC delete "client-scopes/$id/protocol-mappers/models/$mapper" -r "$REALM" + fi + $KC create "client-scopes/$id/protocol-mappers/models" -r "$REALM" \ + -s "name=$scope_name" -s protocol=openid-connect -s protocolMapper=oidc-audience-mapper \ + -s "config.\"$audience_key\"=$audience_value" \ + -s 'config."access.token.claim"=true' \ + -s 'config."introspection.token.claim"=true' + echo "Set audience on $scope_name" + SCOPE_ID="$id" +} + +upsert_scope braid-mcp-audience included.custom.audience "$MCP_CLIENT" +# Realm default, so a client that registers itself gets it without being told. +$KC update "realms/$REALM/default-default-client-scopes/$SCOPE_ID" >/dev/null 2>&1 || true + +upsert_scope braid-api-audience included.client.audience "$API_CLIENT" +mcp_id=$($KC get clients -r "$REALM" -q "clientId=$MCP_CLIENT" --fields id --format csv --noquotes 2>/dev/null | head -1) +$KC update "clients/$mcp_id/default-client-scopes/$SCOPE_ID" -r "$REALM" >/dev/null 2>&1 || true +echo "Gateway can now exchange for the API audience." + +# Every account arrives from Google, so the username form is a dead hop. +# A default on the redirector skips it, for an MCP client too, +# which cannot pass `kc_idp_hint` of its own. +redirector_row=$($KC get "authentication/flows/browser/executions" -r "$REALM" \ + --fields id,providerId,authenticationConfig --format csv --noquotes 2>/dev/null \ + | grep ",identity-provider-redirector") +redirector=$(echo "$redirector_row" | cut -d, -f1 | head -1) +# The listing carries the config id once attached, +# which is the only signal saying whether this ran before. +# The config endpoint answers neither way. +existing=$(echo "$redirector_row" | cut -d, -f3 | head -1) +if [ -n "$redirector" ]; then + if [ -z "$existing" ]; then + $KC create "authentication/executions/$redirector/config" -r "$REALM" \ + -s alias=google-by-default \ + -s config.defaultProvider=google + echo "Browser sign-in now goes straight to Google." + else + echo "Browser sign-in already has a default provider." + fi +else + echo "WARNING: could not find the identity provider redirector." +fi + +echo "Done." diff --git a/package.json b/package.json index b31bc724..669cce30 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "description": "Domain model framework that merges product intent with shipped code into a single HITL-validated model the whole team can read.", "license": "MIT", "engines": { - "node": ">=20", + "node": ">=22", "pnpm": ">=10" }, "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 6fb3e63f..85346257 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -18,6 +18,9 @@ "bin", "dist" ], + "engines": { + "node": ">=22" + }, "scripts": { "build": "tsc -p tsconfig.json", "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json", diff --git a/packages/core/README.md b/packages/core/README.md index 9bc94d4e..170bd306 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -7,7 +7,7 @@ Braid keeps a product's intent and its code aligned by extracting both into one Core is the framework layer every other package builds on. It owns the rules and leaves the plumbing to others. - **The Model**: The knowledge graph as a domain aggregate, with the invariants that keep it valid. -- **The Flows**: Review, batch, and reactor services that turn agent output into committed graph changes. +- **The Flows**: Review, batch, and reactor services that turn agent output into committed graph changes, plus the view service that turns the graph back into something a person reads. - **The Ports**: Interfaces for storage, agents, and history, implemented by sibling packages rather than here. ## Structure @@ -27,10 +27,10 @@ src/ - **domain**: Entities, ports, and value objects, one folder per aggregate. Pure, with no I/O. - **model/**: The `Model` aggregate with its repository and serializer. - - **hitl/**: `Proposal` and `ClarifyTicket` with their repositories. + - **hitl/**: `Proposal` and `Clarification` with their repositories, and the visibility rule they share. Both are handoffs, a point a run reached that only a person can settle, which is why one queue holds them and one permission covers them. - **plugin/**: Port interfaces for ontology, storage, agents, and loaders. - **validation/**: Pure graph invariants, such as evidence, orphan edges, and the ontology type and structural rules. -- **application**: Services that run one use case each, such as `HITLService`, `BatchService`, and `ReactorService`. +- **application**: Services that run one use case each, such as `HITLService`, `BatchService`, `ReactorService`, and `ViewService`. - **infrastructure**: In-memory default adapters for the domain ports. Vendor adapters such as Kuzu live in sibling packages. ## Naming diff --git a/packages/ontology-ddd/README.md b/packages/ontology-ddd/README.md index 9ff32c9f..407bb82c 100644 --- a/packages/ontology-ddd/README.md +++ b/packages/ontology-ddd/README.md @@ -7,7 +7,7 @@ Braid extracts a product's intent and code into one knowledge graph. `@braidhq/o The package is an `OntologyPlugin` for DDD. It says what a DDD graph is made of, and supplies the reasoning that fills it. - **The Vocabulary**: Eight node types (bounded context, aggregate, command, query, event, rule, actor, policy) and fifteen edge types, each tagged with the sub-domain and canonical source it comes from. -- **The Skills**: The `ddd:extract`, `ddd:clarify`, and `ddd:reconcile` SKILL.md prompts, plus the shared reference docs they all consult. Each directory is the bare verb, the `ddd` namespace comes from the ontology. +- **The Skills**: The `ddd:scan`, `ddd:extract`, `ddd:clarify`, and `ddd:reconcile` SKILL.md prompts, plus the shared reference docs they all consult. Each directory is the bare verb, the `ddd` namespace comes from the ontology. Each declares its `order` and its `label`, which is what Build reads to draw the pipeline across the top. - **The Binding**: The required source roles, and the batch and reactor loop that drives per-unit extraction with periodic reconcile checkpoints. ## Structure @@ -20,6 +20,7 @@ src/ ├── types.ts the DDDNodeType and DDDEdgeType enums └── index.ts skills/ +├── scan/ decompose a codebase into extraction units when no intent docs exist ├── extract/ per-unit extraction prompt ├── clarify/ clarification prompt ├── reconcile/ cross-link the slices and validate the whole graph @@ -34,7 +35,7 @@ skills/ A DDD graph reads outward from a bounded context: a `contains` edge holds its aggregates, an aggregate `accepts` commands and queries, a command `emits` events, an event `triggers` downstream work, and a policy `enacts` the command that reacts to it. Operations are `constrainedBy` rules and `performedBy` actors. A separate Context Mapping family wires bounded contexts to one another, partnership, conformist, anticorruption layer, and the rest. -Editing a type here flows through the ontology contract to Studio's palette and legend, the structural and ontology-type validators, and the `GET /workspaces/:ws/ontology` API, with no change needed on those sides. +Editing a type here flows through the ontology contract to Studio's palette and legend, the structural and ontology-type validators, and the `GET /workspaces/:ws/ontology` API, with no change needed on those sides. `renderHint` carries further than the canvas. The document projection reads it to decide what a page covers and how it nests, so `container: true` opens a document and an `expandedUnder` chain gives its sections. ## Boundaries diff --git a/packages/schema/README.md b/packages/schema/README.md index 2c385a09..b2125101 100644 --- a/packages/schema/README.md +++ b/packages/schema/README.md @@ -9,6 +9,7 @@ Schema is the root of the monorepo. It says what data looks like, and nothing ab - **The Shapes**: Every entity, message, and config as a zod schema with an inferred type, so validation and typing come from one definition. - **The Ids**: Branded identifiers, so a `NodeId` can never be passed where a `ProposalId` belongs. - **The Vocabulary**: The closed enums and open brands that decide which sets are fixed and which are extensible. +- **The Render Calls**: `RenderBlock` and its ten calls. Because this is the one layer the server and Studio both see, a single zod object is the route body, the MCP tool schema, and the renderer's type at once. ## Structure @@ -25,17 +26,18 @@ src/ The modules group into a few families. -- **Graph**: `model`, `ontology`, `graph-validation`. The graph shapes and their type descriptors. -- **Review**: `proposal`, `proposal-preview`, `clarify`. The HITL artifacts a human approves. -- **Orchestration**: `batch`, `reactor`, `skill`, `source-unit`. Records of automated runs over sources. -- **Config**: `source`, `mcp`, `storage`, `agent`, `workspace`. The product manifest and what it declares. -- **Cross-Cutting**: `common`, `error`, `event`, `history`, `user`, `view`, `plugin`. Primitives and contracts shared across the rest. +- **Graph**: `model`, `ontology`, `graph-validation`, `reference`. The graph shapes, their type descriptors, and the evidence a claim rests on. +- **Handoff**: `handoff`, `proposal`, `proposal-preview`, `clarification`. A point a run reached that only a person can settle, always from a run to a person and never the reverse. +- **Output**: `block`. The typed calls a run renders with, and therefore what every surface draws. +- **Orchestration**: `batch`, `reactor`, `skill`, `source-unit`, `source-sync`, `coverage`. Records of automated runs over sources, and what the model has so far made of each document. +- **Config**: `source`, `mcp`, `storage`, `agent`, `embedding`, `workspace`. The product manifest and what it declares. +- **Cross-Cutting**: `common`, `error`, `event`, `history`, `user`, `capability`, `locale`, `view`, `plugin`. Primitives and contracts shared across the rest. ## Boundaries These are the rules that keep schema a pure contract. They are enforced in review. -- **Validated or Shared**: A shape earns a place here when it crosses a trust boundary, either parsed from outside the process (an HTTP body, a file on disk, config) or shared as a wire contract between packages (the SSE event stream). Pure in-process types stay in the package that uses them. +- **Validated or Shared**: A shape earns a place here when it crosses a trust boundary, either parsed from outside the process (an HTTP body, a file on disk, config) or shared as a wire contract between packages (the SSE event stream, a render call). Pure in-process types stay in the package that uses them. - **Only Zod Lives Here**: Every schema is defined once in this package. No other package redeclares a shape. - **No Side Effects**: Shapes, validation, and pure helpers only. No I/O, and no import from another Braid package. - **Closed or Open**: A fixed set is a `z.enum`, an extensible one is a branded string, and that choice is the extension boundary for plugins. diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 37ad7dfc..7d0ecc3a 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -37,7 +37,7 @@ Each factory maps a declarative spec to one plugin type on a distinct axis. All - **`defineOntologyPlugin`**: Node and edge types, validators, and the skills bundled with a domain. Composes over a base via `extends`, auto-attaches the type and structural validators, and defaults the skill namespace to the ontology id. Id `ontology.`. - **`defineSourceLoaderPlugin`**: How a source kind provisions and syncs its content into a workspace, plus an optional webhook capability. Id `source-loader.`. - **`defineStoragePlugin`**: How a storage kind builds its per-process `ModelRepository`. Id `storage.`. -- **`defineViewGeneratorPlugin`**: How a view kind renders a model snapshot into an artifact. Id `view-generator.`. +- **`defineViewGeneratorPlugin`**: How a view kind is written. Each form declares what it asks before it is written, what `format` it writes, and the skill that writes it, and the kind declares through `subjects` which nodes it can be written about. Id `view-generator.`. - **`defineAgentPlugin`**: How an agent kind constructs its runtime binding. The binding is built synchronously because the subprocess spawn is lazy. Id `agent.`. ## Boundaries diff --git a/packages/server/README.md b/packages/server/README.md index aa4bd0dd..7c7a4a74 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -9,6 +9,8 @@ Server is the composition root and the presentation layer. It turns the framewor - **The Wiring**: The composition root that binds core's ports to concrete adapters, the one place infrastructure is instantiated. - **The Adapters**: Filesystem and git implementations of core's repositories and history, plus the subprocess runner that executes skills. - **The API**: A REST and SSE surface built on Hono, one router per resource, with authentication, workspace scoping, and an OpenAPI 3 spec at `GET /openapi.json`. +- **The Run Wire**: A run is read over AG-UI at `GET /workspaces/:workspaceId/agui`. The translation from an agent's own events sits at this edge and nowhere else. +- **The Render Calls**: The routes a skill calls to draw its output. Each is an operation on the spec above, so it is also a named MCP tool with a schema, and the agent fills in fields rather than formatting prose. ## Positioning: framework kernel and worldview preset @@ -35,11 +37,12 @@ src/ ├── composeFsApp.ts the coding preset, filesystem and vendor adapters ├── startup.ts blocking per-workspace boot steps ├── authMode.ts AuthMode strategy, localTrust and authenticated -├── routes/ one router per resource +├── routes/ one router per resource, including agui, blocks, and views ├── middleware/ auth, cors, error mapping, workspace scoping ├── policy/ authorization rules └── infrastructure/ adapters behind core ports, one folder per domain concern ├── hitl/ proposal and clarification stores + ├── agui/ the translation from agent events to AG-UI events ├── workspace/ workspace repository, registry, discovery, PRODUCT.md writer ├── skill/ skill registry, run store, subprocess runner and its event stream ├── source/ source unit observations, digests, intent listing @@ -55,6 +58,7 @@ src/ - **startup**: The two boot passes, `startupBeforeServe` and `startupAfterServe`. See Startup below for the full order. - **infrastructure**: The real adapters behind core's ports, grouped by domain concern to mirror `core/domain`, never by storage technology. Each folder owns one aggregate's adapter, so a future SQLite or Postgres store lands beside the filesystem one instead of in a separate `sql/` tree. `hitl/` persists proposals and clarifications, `workspace/` the graph and workspace files, `history/` records every change as a git commit, `skill/` runs a skill as a subprocess and streams its events back. `_shared/` holds the cross-cutting fs plumbing, its underscore marking it as the one folder that is not a domain concern, matching `routes/_shared.ts`. `users/ auth/ secrets/ oauth/` are host services with no core port. - **routes**: One `createXxxRouter(deps)` per resource, each taking only the services it needs. Bodies validate through zod, path ids parse through their branded schema. +- **agui**: The only place an agent's own event shape is known. Everything downstream reads the normalised protocol form, so a second agent that speaks AG-UI natively moves this translation rather than the surfaces. - **middleware**: The cross-cutting edge. Auth resolves the user, workspace middleware scopes and authorizes the request, and the error middleware maps `BraidError` subclasses to problem+json status codes. ## Startup @@ -227,7 +231,7 @@ These are the rules for anyone editing server. They are enforced in review rathe Server sits at the outer edge, above core and the plugin packages the coding preset bundles as defaults. -- **Depends On**: `@braidhq/core` and `@braidhq/schema`, `hono` for HTTP, `simple-git` for history, and, only through the coding preset, the default plugin bundle of `storage-kuzu`, `agent-claude-code`, `ontology-ddd`, and `source-loader-*`. The kernel itself needs none of the plugin packages; they leave with the preset when it extracts. See Positioning. +- **Depends On**: `@braidhq/core` and `@braidhq/schema`, `hono` for HTTP, `@ag-ui/core` and `@ag-ui/encoder` for the run wire, `simple-git` for history, and, only through the coding preset, the default plugin bundle of `storage-kuzu`, `agent-claude-code`, `ontology-ddd`, and `source-loader-*`. The kernel itself needs none of the plugin packages; they leave with the preset when it extracts. See Positioning. - **Consumed By**: `cli` and the `desktop` Tauri shell, which run it as their backend. ## MCP Gateway @@ -236,7 +240,9 @@ Nothing here implements MCP. An off-the-shelf translator, `openapi-mcp-gateway`, ### Per skill run -Skills run as coding-agent subprocesses and reach this server's REST API as MCP tools, not by curl. That gateway speaks stdio, is spawned per run and torn down with it, and exposes the whole spec as `braid-core`. No long-lived process, no open port, no network auth to manage. +Skills run as coding-agent subprocesses and reach this server's REST API as MCP tools, not by curl. That gateway speaks stdio, is spawned per run and torn down with it, and is registered as `braid-core`. No long-lived process, no open port, no network auth to manage. + +What it reads is not the whole spec. An operation declares which run categories may see it, beside the route it describes, and `GET /openapi/runs/:category/openapi.json` serves the narrowed result. An `ask` run is not asked to refrain from proposing, `proposal-create` is not in the spec it is handed. An absent operation costs neither a place in the tool list nor the tokens to describe it. That route is public, since the gateway reads it before a run exists to authenticate as. This scopes what a run is offered, not what the API accepts. SSE streams and the OAuth HTML callback are deliberately absent from the spec, they are not one-shot MCP tools. The `GET /openapi.json` test in `test/app.test.ts` pins that boundary. diff --git a/packages/source-loader-github/src/GithubSourceLoaderPlugin.ts b/packages/source-loader-github/src/GithubSourceLoaderPlugin.ts index a9a08af8..9eadb728 100644 --- a/packages/source-loader-github/src/GithubSourceLoaderPlugin.ts +++ b/packages/source-loader-github/src/GithubSourceLoaderPlugin.ts @@ -423,7 +423,19 @@ async function fetchLinkedMergedPRs( const payload = await response.json() as TimelineQueryResponse if (payload.errors && payload.errors.length > 0) throw new Error(`githubLoader: GraphQL errors for issue ${issueNumber}: ${payload.errors.map(e => e.message).join('; ')}`) - const nodes = payload.data?.repository?.issue?.timelineItems?.nodes ?? [] + // A GitHub App reads only the repositories it is installed on, + // and an uninstalled one answers with a null repository, not an error. + // Reading that as "no linked PR" files every issue as unrealized, + // and reports a clean sync that wrote nothing, + // which is the one outcome nobody can diagnose from the outside. + const repository = payload.data?.repository + if (!repository || !repository.issue) { + throw new Error( + `githubLoader: GitHub returned no issue ${issueNumber} on ${config.owner}/${config.repo}. ` + + 'Install the GitHub App on that repository and grant it read access to issues and pull requests.', + ) + } + const nodes = repository.issue.timelineItems?.nodes ?? [] const seen = new Set() const out: MergedPRRef[] = [] for (const node of nodes) { diff --git a/packages/source-loader-mcp/README.md b/packages/source-loader-mcp/README.md index 5a7fc36d..11437e99 100644 --- a/packages/source-loader-mcp/README.md +++ b/packages/source-loader-mcp/README.md @@ -4,6 +4,25 @@ Mirrors an API-backed source into markdown by calling one MCP tool until its pages run out. One file per item, which is what makes an issue tracker or a wiki a tracked source rather than a lookup an agent happens to make mid-run. +## Role + +The package is a `SourceLoaderPlugin` for the `mcp` loader kind. It fills a directory and owns nothing else. + +- **The Walk**: Calls one configured tool until its pages run out, since a gateway maps one call onto one upstream request and does not loop. +- **The Mapping**: Reads a reply through configured paths rather than a fixed envelope, so a server nobody here controls is named rather than adapted to. +- **The Mark**: Records the newest `updatedAt` it saw, so the next sync starts from there instead of re-reading the history. + +## Structure + +``` +src/ +├── McpSourceLoaderPlugin.ts the defineSourceLoaderPlugin value, the cursor file, the write +├── config.ts the loader config schema and its defaults +├── client.ts the streamable-http session and one tool call +├── mirror.ts the walk across the tool's pages +└── item.ts one item into one markdown file +``` + ## The Shape It Expects The loader knows a tool name and where the parts of a reply live. Nothing else. @@ -90,16 +109,14 @@ paging cannot advance at all. is raised, because a sync that quietly mirrors nothing is worse than one that stops. -## Transport +## Boundaries -Streamable HTTP only. The MCP server is a process the operator runs, so this -speaks to it and never manages its lifetime. Handling stdio would put spawning, -timeouts, and orphan reaping inside the Braid server for a case nobody has -asked for yet. +- **Streamable HTTP Only**: The MCP server is a process the operator runs, so this speaks to it and never manages its lifetime. Handling stdio would put spawning, timeouts, and orphan reaping inside the Braid server for a case nobody has asked for yet. +- **A Directory, Not a Source Kind**: `loader.kind: mcp` is not `kind: mcp`, the source kind in the schema. A source using this loader is a `filesystem` source, because what it produces is a directory of files. Only the way that directory gets filled is MCP. The two never appear in the same position. +- **No Credential On Disk**: `${VAR}` resolves against the server's environment at sync time, so `PRODUCT.md` records which variable is needed and never its value. +- **Mirrors, Never Reads**: The loader fills a directory. What the documents mean is the ontology's business, and nothing here knows an issue from a wiki page. -## A Note On The Name +## Dependencies -`loader.kind: mcp` is not the same as `kind: mcp`, the source kind in the -schema. A source using this loader is a `filesystem` source, because what it -produces is a directory of files. Only the way that directory gets filled is -MCP. The two never appear in the same position. +- **Depends On**: `@braidhq/core` for the port, `@braidhq/schema` for shared types, `@braidhq/sdk` for `defineSourceLoaderPlugin`, `@modelcontextprotocol/client` for the connection, and `yaml` and `zod`. +- **Consumed By**: The server composition root, where `composeFsApp` registers it in the default plugin bundle. diff --git a/packages/studio/README.md b/packages/studio/README.md index 82cb1c0f..f8db5ac2 100644 --- a/packages/studio/README.md +++ b/packages/studio/README.md @@ -1,13 +1,14 @@ # @braidhq/studio -Braid keeps a product's intent and its code aligned in one knowledge graph. `@braidhq/studio` is the web UI a human uses to see that graph and steer it. It renders the graph, the review queues that gate every change to it, and the skill runs that produce those changes, talking to the server over REST and a live event stream. +Braid keeps a product's intent and its code aligned in one knowledge graph. `@braidhq/studio` is the web UI a human uses to see that graph and steer it. It renders the graph, the queue that gates every change to it, and the skill runs that produce those changes. State comes over REST with a live event stream beside it, and a run comes over AG-UI. ## Role Studio is the presentation layer. It reads and displays server state, and it turns a reviewer's decisions into API calls, but it holds no authority of its own. -- **The Surfaces**: The graph canvas and table, the Proposals and Clarification review queues, the Actions and Batch skill panels, Activity, and History, one page per workspace surface. -- **The Live Loop**: A server-sent event stream that invalidates cached queries as runs finish and proposals apply, so the UI reflects server state without polling. +- **The Surfaces**: Six, one per thing somebody opens Studio to do. Ask is a canvas for an answer, Graph is the canvas and table over the model, Build is every source document and what the model has made of it, Inbox is one queue holding both the questions a run stopped on and the changes it proposed, Documents shelves what has been written off the graph, and History is the commit record. +- **The Block Renderers**: One component per `RenderBlock` call. A block carries no layout, so the same renderers draw a live answer on Ask and a stored document on Documents. +- **The Live Loop**: A server-sent event stream that invalidates cached queries as runs finish and proposals apply, so the UI reflects server state without polling. A run itself arrives separately, over AG-UI. - **The Advisory Gate**: A client-side policy layer that disables and hides what a viewer may not do, mirroring the server's rules for a quiet UI while the server stays the real enforcer. ## Structure @@ -24,9 +25,9 @@ src/ └── policy/ client-side capability checks ``` -- **lib**: The non-visual core. The REST client (`api.ts`), the SSE streams (`sse.ts`, `useWorkspaceEvents.ts`), the TanStack Query hooks (`queries.ts`), the run store that outlives page mounts (`runStore.ts`), per-remote auth and tokens, and the hash-routing state. +- **lib**: The non-visual core. The REST client (`api.ts`), the SSE streams (`sse.ts`, `useWorkspaceEvents.ts`), the AG-UI client and its normalising layer (`agui/`), the block helpers (`blocks/`) and reference resolution (`references/`), the TanStack Query hooks (`queries.ts`), the run store that outlives page mounts (`runStore.ts`), per-remote auth and tokens, and the hash-routing state. - **pages**: One file per surface, fed a `workspaceId`. Every page is workspace-scoped except `Login` and `Settings`. -- **components**: App-level widgets, the `graph/` canvas and table layer over `@xyflow/react` and `dagre`, the `SkillTranscript/` renderer for a live `SkillEvent` stream, and the `ui/` shadcn and Radix primitives. +- **components**: App-level widgets, the `graph/` canvas and table layer over `@xyflow/react` and `dagre`, the `blocks/` renderers for a `RenderBlock` sequence, `handoff/` for the two kinds of card the Inbox holds, `references/` for evidence that resolves to its own host, and the `ui/` shadcn and Radix primitives. - **policy**: The capability registry and checks a component asks through `useWorkspacePolicy`, kept in lock-step with the server's own resolution. ## Data Flow @@ -35,7 +36,8 @@ State comes from the server and returns to it as intent. Studio caches and rende - **Reads**: TanStack Query fetches through `api.ts`. A per-workspace SSE subscription invalidates the matching query keys as events arrive, so a finished run or applied proposal refreshes the view. - **Writes are proposals**: The canvas never edits the graph directly. A skill run emits a `Proposal`, a reviewer sees its diff and validation issues, and applying it POSTs to the server, which commits and emits the event that refreshes the graph. -- **Runs stream**: `runStore` holds in-flight and completed runs outside React state, so leaving and returning to a page keeps the transcript and lets a multi-turn conversation resume. +- **Runs stream over AG-UI**: A run is read from `GET /workspaces/:workspaceId/agui` with AG-UI's own `HttpAgent`, not a client written here. Using the stock client is the check that the endpoint is the protocol rather than something shaped like it. `lib/agui` normalises the events, and `runStore` holds in-flight and completed runs outside React state, so leaving and returning to a page keeps the transcript and lets a multi-turn conversation resume. +- **Blocks decide nothing about layout**: A run's output arrives as typed blocks. The surface decides what the sequence becomes, which is why an answer and a document share one set of renderers. ## Design @@ -66,5 +68,5 @@ These are the rules that keep Studio a thin, honest client. They are enforced in Studio sits at the edge of the monorepo, downstream of the schema and the running server. -- **Depends On**: `@braidhq/schema` for every shape it renders, a live `@braidhq/server` to talk to, and the UI stack: `react`, `@tanstack/react-query`, `@xyflow/react` with `dagre`, `mermaid`, `react-markdown`, `cmdk`, Radix and `tailwindcss`, and `@tauri-apps/api` for the desktop shell. +- **Depends On**: `@braidhq/schema` for every shape it renders, a live `@braidhq/server` to talk to, `@ag-ui/client` and `@ag-ui/core` for reading a run, and the UI stack: `react`, `@tanstack/react-query`, `@xyflow/react` with `dagre`, `mermaid`, `react-markdown`, `cmdk`, Radix and `tailwindcss`, and `@tauri-apps/api` for the desktop shell. - **Consumed By**: The `desktop` Tauri shell and `@braidhq/server`, both of which serve the built assets this package publishes, and any browser pointed at a Braid server. diff --git a/packages/studio/src/components/AddSourceDialog.tsx b/packages/studio/src/components/AddSourceDialog.tsx index 40e43b0a..d8f6c6c9 100644 --- a/packages/studio/src/components/AddSourceDialog.tsx +++ b/packages/studio/src/components/AddSourceDialog.tsx @@ -307,7 +307,7 @@ export function AddSourceDialog({ workspaceId, open, onOpenChange, onAdded }: Ad

{t('sources.addDialog.githubAuthPrefix')} {' '} - $GH_TOKEN + BRAID_GITHUB_CLIENT_ID {' '} {t('sources.addDialog.githubAuthSuffix')}

diff --git a/packages/studio/src/components/InFlightRunBanner.tsx b/packages/studio/src/components/InFlightRunBanner.tsx index 47ff6c3e..05c32aaa 100644 --- a/packages/studio/src/components/InFlightRunBanner.tsx +++ b/packages/studio/src/components/InFlightRunBanner.tsx @@ -1,9 +1,11 @@ +import type { Locale } from '@braidhq/schema' +import { localize } from '@braidhq/schema' import { useMutation } from '@tanstack/react-query' import { X } from 'lucide-react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { api } from '@/lib/api' -import { useRuns } from '@/lib/queries' +import { useRuns, useSkills } from '@/lib/queries' import { TopBanner } from './TopBanner' import { Button } from './ui/button' @@ -43,8 +45,14 @@ function ActiveBanner({ workspaceId, runId, skillId, startedAt }: { skillId: string startedAt: string }) { - const { t } = useTranslation() + const { t, i18n } = useTranslation() + const { data: skills } = useSkills(workspaceId) const elapsed = useElapsed(startedAt) + // A skill id is an address, so `/ddd:extract` on a banner shows plumbing. + // The skill names itself for a reader, localised like the ontology's types. + // Absent, the id stands, which is at least true. + const declared = skills?.items.find(skill => skill.id === skillId)?.frontmatter.braid.label + const label = declared ? localize(declared, i18n.language as Locale) : `/${skillId}` const cancel = useMutation({ mutationFn: () => api.cancelRun(workspaceId, runId), }) @@ -52,7 +60,7 @@ function ActiveBanner({ workspaceId, runId, skillId, startedAt }: { return ( /oauth/github/callback. The App also has to be installed on the repository it reads.', unknownLoaderHintPrefix: 'This loader plugin is registered on the server but Studio does not ship a per-field config for it. To use it, edit', unknownLoaderHintMiddle: 'directly and add the', unknownLoaderHintSuffix: 'config under this source.', diff --git a/packages/studio/src/locales/zh-Hant/sources.ts b/packages/studio/src/locales/zh-Hant/sources.ts index 8eff5640..c7a87115 100644 --- a/packages/studio/src/locales/zh-Hant/sources.ts +++ b/packages/studio/src/locales/zh-Hant/sources.ts @@ -39,8 +39,8 @@ const sources: typeof en = { stateLabel: '狀態', labelsLabel: '標籤 (CSV,選填)', includeComments: '包含留言', - githubAuthPrefix: '驗證:伺服器會讀取', - githubAuthSuffix: '進行同步。若未設定,將套用 GitHub 每小時 60 次的匿名速率限制。', + githubAuthPrefix: '驗證:加入後到來源設定完成連結。伺服器需要一組 GitHub App,client id 填入', + githubAuthSuffix: ',secret 一併設定,並在 App 上註冊 /oauth/github/callback 這個 callback。另外 App 必須安裝到它要讀取的 repository 上。', unknownLoaderHintPrefix: '這個載入器外掛已在伺服器上註冊,但 Studio 未內建對應的欄位設定。若要使用,請直接編輯', unknownLoaderHintMiddle: ',並在這個來源底下加入', unknownLoaderHintSuffix: '設定。', diff --git a/packages/studio/src/pages/Documents.tsx b/packages/studio/src/pages/Documents.tsx index 12b3c26e..1cd304a0 100644 --- a/packages/studio/src/pages/Documents.tsx +++ b/packages/studio/src/pages/Documents.tsx @@ -133,7 +133,12 @@ export function DocumentsPage({ workspaceId, onSelectNode }: { onChange={event => setQuery(event.target.value)} /> + {/* The empty state shares this box instead of sitting below it, */} + {/* since two flex-1 siblings split the column and leave it low. */}
+ {shelves.length === 0 && ( + + )} {shelves.map(shelf => (
{shelves.length > 1 && ( @@ -158,11 +163,6 @@ export function DocumentsPage({ workspaceId, onSelectNode }: {
))}
- {shelves.length === 0 && ( -
- -
- )} )} > @@ -222,7 +222,9 @@ function DocumentRow({ group, name, forms, openPath, onOpen, locale, staleLabel
{name} {group.stale && ( - + // Every other outline badge here is uppercase with wider tracking. + // This one was the exception, so a status read as a phrase here. + {staleLabel} )} diff --git a/packages/view-generator-doc/skills/reference/SKILL.md b/packages/view-generator-doc/skills/reference/SKILL.md index 9cbf6568..e00ad28f 100644 --- a/packages/view-generator-doc/skills/reference/SKILL.md +++ b/packages/view-generator-doc/skills/reference/SKILL.md @@ -5,6 +5,9 @@ argument-hint: "[path-to-material]" disable-model-invocation: true braid: category: generate + label: + en: Reference + zh-Hant: 手冊 summary: Write a container up as a document to look things up in hidden: true required-env: [BRAID_WORKSPACE, BRAID_RUN_ID, BRAID_SHARED_REFERENCE] diff --git a/packages/view-generator-doc/skills/tutorial/SKILL.md b/packages/view-generator-doc/skills/tutorial/SKILL.md index 0861099b..023b50fe 100644 --- a/packages/view-generator-doc/skills/tutorial/SKILL.md +++ b/packages/view-generator-doc/skills/tutorial/SKILL.md @@ -5,6 +5,9 @@ argument-hint: "[path-to-material] [depth=…]" disable-model-invocation: true braid: category: generate + label: + en: Tutorial + zh-Hant: 教學 summary: Teach a container to someone new to the subject hidden: true required-env: [BRAID_WORKSPACE, BRAID_RUN_ID, BRAID_SHARED_REFERENCE]