diff --git a/.github/workflows/hub-tests.yml b/.github/workflows/hub-tests.yml
new file mode 100644
index 0000000..4ed2357
--- /dev/null
+++ b/.github/workflows/hub-tests.yml
@@ -0,0 +1,22 @@
+name: Hub tests
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+ - name: Local hub structure
+ run: node --test tests/hub.test.mjs
+ - name: Live essays still ship
+ run: node --test tests/live-essays.test.mjs
+ - name: Deployed hub parity
+ run: node --test tests/deployed-parity.test.mjs
+ env:
+ LIVE_HUB_URL: https://zishanalikhan.com/agents-orchestrated/
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a7b1b87
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+.vercel
+.DS_Store
+docs/agents-orchestrated
diff --git a/docs/.nojekyll b/docs/.nojekyll
new file mode 100644
index 0000000..e69de29
diff --git a/docs/assets/codemap.js b/docs/assets/codemap.js
new file mode 100644
index 0000000..bd57d5b
--- /dev/null
+++ b/docs/assets/codemap.js
@@ -0,0 +1,385 @@
+/* Visual codemap for Agents, Orchestrated.
+ Click a node → inspector. Hash (#ep01|#ep02|#ep03|#shared|#stack) selects a view. */
+
+const REPOS = {
+ ep01: "https://github.com/letmereviewyourcode/what-are-loops",
+ ep02: "https://github.com/letmereviewyourcode/camunda-agui-wealth-demo",
+ ep03: "https://github.com/letmereviewyourcode/camunda-ap2-payments-demo",
+};
+
+const VIEWS = {
+ stack: {
+ title: "The series as one system",
+ subtitle: "Three repos, one control plane. Humans at the top, cryptography at the bottom. Click any box.",
+ },
+ ep01: {
+ title: "EP 01 · what-are-loops",
+ subtitle: "A verification loop that made a film about verification loops. The Remotion renderer is private; this repo is the spec, the check, and the result.",
+ },
+ ep02: {
+ title: "EP 02 · camunda-agui-wealth-demo",
+ subtitle: "AURUM. One SSE stream, three surfaces, Camunda holding the token. Expand a layer by clicking it.",
+ },
+ ep03: {
+ title: "EP 03 · camunda-ap2-payments-demo",
+ subtitle: "Open mandate → closed mandate → merchant verifies the chain. Four demo paths, one spending table.",
+ },
+ shared: {
+ title: "Shared DNA",
+ subtitle: "EP 03 did not start from a blank folder. These files recur, renamed only when the job changed.",
+ },
+};
+
+const NODES = {
+ stack: [
+ { id: "humans", x: 80, y: 28, w: 840, h: 56, label: "Humans — customer · advisor · operator · merchant", sub: "three browsers, one truth", fill: "#161618", repo: "ep02",
+ kicker: "Surface",
+ body: "Customer, advisor, and operator views subscribe to the same AG-UI stream. The operator view is the raw wire — the “don’t take my word for it” surface from the films. EP 03 adds the merchant (TicketVault) as a fourth party that independently verifies signatures.",
+ path: "frontend/src/views/*", links: ["ep02", "ep03"] },
+ { id: "agui", x: 80, y: 108, w: 840, h: 56, label: "AG-UI · Server-Sent Events", sub: "the wire — EP 02 invented the pattern, EP 03 reused it", fill: "#13261f", repo: "ep02",
+ kicker: "Protocol",
+ body: "Typed events over one EventSource: TEXT_MESSAGE, TOOL_CALL, GENERATIVE_UI, USER_TASK, RUN_FINISHED. The UI is a reducer over that stream. Swap a donut for a mandate card — same wire.",
+ path: "frontend/src/agui-client.js · backend/src/sseHub.js" },
+ { id: "loop", x: 80, y: 188, w: 840, h: 56, label: "The loop — generate → run → check → repeat", sub: "EP 01 named it; every later episode runs one", fill: "#2a2114", repo: "ep01",
+ kicker: "EP 01",
+ body: "A loop is an agent that runs itself. Continuation is either a trigger or a goal. Verification is the moat: independent judge, tests-until-green, or artifacts a human can audit. “Done” is evidence, not vibes.",
+ path: "LOOPS_DEEP_DIVE.md" },
+ { id: "bff", x: 80, y: 268, w: 400, h: 64, label: "Node BFF", sub: "sseHub · workers · task poller", fill: "#161618", repo: "ep02",
+ kicker: "Bridge",
+ body: "Thin on purpose. Workers implement tool jobs and emit AG-UI events. The LLM loop itself lives in Camunda (AI Agent Connector), not in this Node process — that’s the point of EP 02’s learning guide.",
+ path: "backend/src/" },
+ { id: "camunda", x: 520, y: 268, w: 400, h: 64, label: "Camunda 8", sub: "BPMN · DMN · Operate · Tasklist", fill: "#161618", repo: "ep02",
+ kicker: "Orchestrator",
+ body: "The process is the orchestrator. User tasks pause a token until a human acts. DMN tables route wealth tiers (EP 02) and spending policy (EP 03). Operate is the audit trail the films keep cutting to.",
+ path: "bpmn/ · dmn/" },
+ { id: "ground", x: 80, y: 360, w: 400, h: 64, label: "Grounding", sub: "KB seed + query as BPMN steps", fill: "#161618", repo: "ep02",
+ kicker: "EP 02",
+ body: "Retrieval is a process step, not a system prompt. client-kb-seed.bpmn / client-kb-query.bpmn + kbQueryWorker.js. Guardrail: what it can’t verify, it doesn’t invent.",
+ path: "bpmn/client-kb-*.bpmn · backend/src/workers/kbQueryWorker.js" },
+ { id: "auth", x: 520, y: 360, w: 400, h: 64, label: "AP2 authority", sub: "ES256 JWS open → closed mandate", fill: "#1f1c14", repo: "ep03",
+ kicker: "EP 03",
+ body: "jose + ES256. User signs the open checkout mandate (cap $7,000). Agent signs the closed mandate bound to a sha256 cart hash. Merchant runs three checks: user sig, agent sig, constraints. Tamper the cap inside the token and checkout is refused — cryptography, not a database flag.",
+ path: "backend/src/mandateService.js" },
+ { id: "policy", x: 80, y: 452, w: 840, h: 56, label: "Governance — DMN first-hit tables, visible in Operate", sub: "wealth routing · spending policy", fill: "#161618", repo: "ep03",
+ kicker: "Policy",
+ body: "EP 02: wealth-routing.dmn sends private-wealth tiers to a human user task. EP 03: spending-policy.dmn (5 rules, first-hit) auto-approves, escalates at 98% of cap, or declines a breach. The table result is linked to the process instance.",
+ path: "bpmn/wealth-routing.dmn · dmn/spending-policy.dmn" },
+ ],
+ ep01: [
+ { id: "goal", x: 360, y: 24, w: 280, h: 52, label: "/goal + spec", sub: "one command, four checks", fill: "#2a2114",
+ kicker: "The loop",
+ body: "The video was not hand-edited. A Claude Code /goal was pointed at VIDEO_SPEC.md + CAPTION_SCRIPT.md. It built the film, then verified itself frame-by-frame before calling the job done. A verification loop, to make a video about verification loops.",
+ path: "VIDEO_SPEC.md", repo: "ep01" },
+ { id: "spec", x: 40, y: 120, w: 200, h: 52, label: "VIDEO_SPEC.md", sub: "objective · storyboard · DoD", fill: "#161618",
+ kicker: "Spec",
+ body: "Source of truth for the /goal. Design system (stage #0B0B0C, amber #EF9F27, Inter + JetBrains Mono), storyboard at 30fps / 1740 frames (~58s), and the locked GOAL_TEXT typed identically into all three tools.",
+ path: "VIDEO_SPEC.md", repo: "ep01" },
+ { id: "captions", x: 260, y: 120, w: 220, h: 52, label: "CAPTION_SCRIPT.md", sub: "locked copy + 4-layer check", fill: "#161618",
+ kicker: "Verification",
+ body: "Every on-screen word, timed. Layer 0 compile; layer 1 frame stills with the correct caption actually on screen; layer 2 content accuracy (shared GOAL_TEXT, 7 map pills, only own imagery); layer 3 full render. The build is not done when an .mp4 exists.",
+ path: "CAPTION_SCRIPT.md", repo: "ep01" },
+ { id: "dive", x: 500, y: 120, w: 220, h: 52, label: "LOOPS_DEEP_DIVE.md", sub: "field guide · the 2×2", fill: "#161618",
+ kicker: "Field guide",
+ body: "What a loop is, the four components (continuation, verification, goal anatomy, locus), and how Claude Code, Codex, Antigravity, and Cursor each implement them as of early 2026. Read this first.",
+ path: "LOOPS_DEEP_DIVE.md", repo: "ep01" },
+ { id: "narr", x: 740, y: 120, w: 220, h: 52, label: "NARRATION.md", sub: "teleprompter.html", fill: "#161618",
+ kicker: "Voice",
+ body: "Voiceover script plus a synced teleprompter for recording on time. LinkedIn autoplays muted — the film is designed to read with sound off; narration is backup.",
+ path: "NARRATION.md · teleprompter.html", repo: "ep01" },
+ { id: "shots", x: 140, y: 230, w: 280, h: 52, label: "Screenshots/", sub: "own captures only", fill: "#161618",
+ kicker: "Guardrail",
+ body: "claude-goal, codex-goal, antigravity-goal, claude-routines. Only the author’s screenshots and headshot appear as real imagery. Conceptual graphics are original, inspired by OpenAI’s Goals cookbook, never copied.",
+ path: "Screenshots/", repo: "ep01" },
+ { id: "out", x: 480, y: 230, w: 280, h: 52, label: "out/", sub: "loops-vertical.mp4 · map-2x2.png", fill: "#161618",
+ kicker: "Result",
+ body: "The rendered film and the two-axis map. The Remotion implementation that produced them is kept private — this repo is the spec, the verification approach, and the result.",
+ path: "out/loops-vertical.mp4", repo: "ep01" },
+ { id: "l0", x: 40, y: 340, w: 220, h: 70, label: "0 · Build & static", sub: "compiles · assets resolve", fill: "#161618",
+ kicker: "Layer 0", body: "Both compositions register. Every asset resolves. Compiles clean. If this fails, nothing else runs.", path: "CAPTION_SCRIPT.md", repo: "ep01" },
+ { id: "l1", x: 280, y: 340, w: 220, h: 70, label: "1 · Frame stills", sub: "caption actually on screen", fill: "#161618",
+ kicker: "Layer 1", body: "Render a still per scene. Confirm the locked caption is on screen, legible, not clipped. Evidence, not vibes.", path: "CAPTION_SCRIPT.md", repo: "ep01" },
+ { id: "l2", x: 520, y: 340, w: 220, h: 70, label: "2 · Content", sub: "GOAL_TEXT identical ×3", fill: "#161618",
+ kicker: "Layer 2", body: "One shared GOAL_TEXT typed identically in all three tools. Every locked string verbatim. Seven map pills correct. Only own imagery.", path: "CAPTION_SCRIPT.md", repo: "ep01" },
+ { id: "l3", x: 760, y: 340, w: 200, h: 70, label: "3 · Full render", sub: "~58s, audio synced", fill: "#2a2114",
+ kicker: "Layer 3", body: "Both .mp4s render error-free, ~58 seconds, audio synced. Only then is the loop allowed to stop.", path: "out/loops-vertical.mp4", repo: "ep01" },
+ ],
+ ep02: [
+ { id: "cust", x: 40, y: 24, w: 180, h: 48, label: "CustomerView", sub: "#/customer", fill: "#13261f",
+ kicker: "Surface", body: "Sarah Chen (straight-through) or James Rodriguez (HITL). Generative cards land here: identity, risk gauge, allocation donut, document requests.", path: "frontend/src/views/CustomerView.jsx", repo: "ep02" },
+ { id: "adv", x: 240, y: 24, w: 180, h: 48, label: "AdvisorView", sub: "#/advisor · desk", fill: "#13261f",
+ kicker: "Surface", body: "The approval queue. Completing a card completes the Camunda user task — the paused token moves.", path: "frontend/src/views/AdvisorView.jsx", repo: "ep02" },
+ { id: "ops", x: 440, y: 24, w: 180, h: 48, label: "OperatorView", sub: "raw AG-UI log", fill: "#13261f",
+ kicker: "Surface", body: "Open #/operator/{pid} next to a customer session and watch the raw wire produce the UI. The film’s honesty device.", path: "frontend/src/views/OperatorView.jsx", repo: "ep02" },
+ { id: "cards", x: 640, y: 24, w: 180, h: 48, label: "Cards.jsx", sub: "generative UI", fill: "#13261f",
+ kicker: "UI", body: "PortfolioAllocationChart, ProjectedGrowthChart, IdentityResultCard, RiskGauge, AccountOpenedCard, UserTaskCard… each is a GENERATIVE_UI component name + props.", path: "frontend/src/components/Cards.jsx", repo: "ep02" },
+ { id: "agui2", x: 280, y: 100, w: 300, h: 48, label: "agui-client.js · useAGUI", sub: "~150 lines, no framework", fill: "#2a2114",
+ kicker: "Client", body: "EventSource in, reducer out. events / cards / pendingTasks / steps. The learning-path item #1 in the README: this is how little a client needs.", path: "frontend/src/agui-client.js", repo: "ep02" },
+ { id: "sse2", x: 280, y: 176, w: 300, h: 48, label: "sseHub.js", sub: "pub/sub keyed by pid", fill: "#2a2114",
+ kicker: "Bus", body: "In-process fan-out. Workers, pollers, and REST handlers call emit(pid, event). Late subscribers get history replay (max 500). A global channel feeds the advisor dashboard.", path: "backend/src/sseHub.js", repo: "ep02" },
+ { id: "intake", x: 40, y: 256, w: 160, h: 48, label: "intakeAgent", sub: "docs by profile", fill: "#161618",
+ kicker: "Worker", body: "Resolves document requirements per customer, emits DocumentRequestCards. Dual intake: form or chat.", path: "backend/src/workers/intakeAgent.js", repo: "ep02" },
+ { id: "ident", x: 220, y: 256, w: 160, h: 48, label: "identityAgent", sub: "KYC · PEP", fill: "#161618",
+ kicker: "Worker", body: "Doc validation, sanctions, PEP. Emits IdentityResultCard.", path: "backend/src/workers/identityAgent.js", repo: "ep02" },
+ { id: "risk", x: 400, y: 256, w: 160, h: 48, label: "riskAgent", sub: "score · tier", fill: "#161618",
+ kicker: "Worker", body: "riskScore, aumTier, investorClass. Emits the gauge the customer actually wants instead of a sentence.", path: "backend/src/workers/riskAgent.js", repo: "ep02" },
+ { id: "kb", x: 580, y: 256, w: 180, h: 48, label: "kbQueryWorker", sub: "grounded answers", fill: "#161618",
+ kicker: "Grounding", body: "RAG: retrieve, then synthesise a cited answer. Emits KbAnswerCard. Guardrail lives here.", path: "backend/src/workers/kbQueryWorker.js", repo: "ep02" },
+ { id: "chat", x: 780, y: 256, w: 180, h: 48, label: "chatTools.js", sub: "~14 tools", fill: "#161618",
+ kicker: "Tools", body: "lookupApplicationStatus, readDocument, estimateTimeline… the deterministic jobs the AI Agent Connector may pick from the ad-hoc sub-process.", path: "backend/src/workers/chatTools.js", repo: "ep02" },
+ { id: "onb", x: 40, y: 344, w: 280, h: 56, label: "wealth-onboarding.bpmn", sub: "intake → KYC → risk → DMN → HITL", fill: "#161618",
+ kicker: "Process", body: "The orchestrator. After risk, wealth-routing.dmn branches STP / ADVISOR_REVIEW / COMPLIANCE_ESCALATION. The AI did not decide to wait — the process did.", path: "bpmn/wealth-onboarding.bpmn", repo: "ep02" },
+ { id: "conv", x: 340, y: 344, w: 300, h: 56, label: "customer-conversation.bpmn", sub: "AI Agent Connector · ad-hoc tools", fill: "#161618",
+ kicker: "Process", body: "THE pattern. A long-lived conversation process. The connector hosts the LLM-with-tools loop inside Camunda, not in Node. Memory persists because agent.context flows turn-to-turn.", path: "bpmn/customer-conversation.bpmn", repo: "ep02" },
+ { id: "dmn2", x: 660, y: 344, w: 300, h: 56, label: "wealth-routing.dmn", sub: "who sees a human", fill: "#2a2114",
+ kicker: "Policy", body: "Decision table. Private-wealth tiers go to a user task with a Camunda Form. Paused token, visible in Operate, completed from the advisor card.", path: "bpmn/wealth-routing.dmn", repo: "ep02" },
+ { id: "seed", x: 40, y: 428, w: 280, h: 48, label: "client-kb-seed / query", sub: "vector store as a step", fill: "#161618",
+ kicker: "Grounding", body: "Embeddings Vector DB connector. Seed chunks the corpus; query retrieves. VECTORDB_LOCAL_SETUP.md if you want it local.", path: "bpmn/client-kb-seed.bpmn", repo: "ep02" },
+ { id: "arch", x: 340, y: 428, w: 280, h: 48, label: "architecture.html", sub: "the poster, in-repo", fill: "#161618",
+ kicker: "Docs", body: "The whole shape on one page — expandable marchitecture. presentation.html for a guided walkthrough. LEARNING_GUIDE.md if you want the prose.", path: "architecture.html", repo: "ep02" },
+ { id: "shadow", x: 640, y: 428, w: 320, h: 48, label: "shadow mode", sub: "zero API keys, full stream", fill: "#13261f",
+ kicker: "Run", body: "Without ANTHROPIC_API_KEY the workers replay a scripted scenario. AG-UI stream, HITL pause, and Operate choreography all work before you spend a cent.", path: "backend/src/seed/scenarios.js", repo: "ep02" },
+ ],
+ ep03: [
+ { id: "shop", x: 40, y: 24, w: 200, h: 52, label: "Shopping agent", sub: "Claude + tools", fill: "#161618",
+ kicker: "Party 1", body: "search_inventory, create_mandate. Holds the open mandate, watches TicketVault for a price drop, signs the closed mandate at purchase time.", path: "backend/src/shoppingAgent.js", repo: "ep03" },
+ { id: "merch", x: 280, y: 24, w: 200, h: 52, label: "Merchant", sub: "TicketVault :5177", fill: "#161618",
+ kicker: "Party 2", body: "Fake merchant site. Independently verifies the mandate chain (user sig, agent sig, constraints) before accepting checkout. The tamper test lands here.", path: "merchant-site/src/", repo: "ep03" },
+ { id: "cred", x: 520, y: 24, w: 200, h: 52, label: "Credentials", sub: "issueToken stub", fill: "#161618",
+ kicker: "Party 3", body: "credentials.issueToken — stubbed. MAKE_IT_REAL.md maps this onto a real credentials provider.", path: "backend/src/workers/index.js", repo: "ep03" },
+ { id: "proc", x: 760, y: 24, w: 200, h: 52, label: "Processor", sub: "checkout rail", fill: "#161618",
+ kicker: "Party 4", body: "merchant.completeCheckout drives TicketVault through AP2 checkout steps 1–6 over SSE (MERCHANT_SITE_STEP).", path: "backend/src/workers/index.js", repo: "ep03" },
+ { id: "openm", x: 80, y: 120, w: 240, h: 56, label: "Open mandate", sub: "user signs the rules", fill: "#2a2114",
+ kicker: "AP2", body: "createOpenCheckoutMandate. Constraints: 2 tickets, target $3,500, cap $7,000. ES256 JWS via jose. The user signed this; the agent cannot rewrite it.", path: "backend/src/mandateService.js", repo: "ep03" },
+ { id: "closed", x: 380, y: 120, w: 240, h: 56, label: "Closed mandate", sub: "agent signs the cart", fill: "#2a2114",
+ kicker: "AP2", body: "closeMandate binds a sha256 checkout hash of the actual cart. This is the v0.2 open→closed lifecycle.", path: "backend/src/mandateService.js", repo: "ep03" },
+ { id: "verify", x: 680, y: 120, w: 260, h: 56, label: "Merchant verify ×3", sub: "user · agent · constraints", fill: "#2a2114",
+ kicker: "AP2", body: "merchantVerifyMandate. Change one number inside the signed token (tamperSignedMandate) and this check fails — even if the agent proceeds and the DMN passes. That is the difference between a protocol and a database record.", path: "backend/src/mandateService.js", repo: "ep03" },
+ { id: "cust3", x: 40, y: 216, w: 200, h: 48, label: "CustomerView", sub: "mandate + cart cards", fill: "#13261f",
+ kicker: "Surface", body: "Watching, waiting, then the four endings: receipt, cryptographic refusal, HITL wait, or MandateBreachCard.", path: "frontend/src/views/CustomerView.jsx", repo: "ep03" },
+ { id: "desk3", x: 260, y: 216, w: 200, h: 48, label: "PaymentDeskView", sub: "HITL at 98% of cap", fill: "#13261f",
+ kicker: "Surface", body: "When the DMN requires a human, the token sits here. Approve and the merchant still verifies the chain.", path: "frontend/src/views/PaymentDeskView.jsx", repo: "ep03" },
+ { id: "ops3", x: 480, y: 216, w: 200, h: 48, label: "OperatorView", sub: "PARTY_HOP strip", fill: "#13261f",
+ kicker: "Surface", body: "Four-party AP2 chain lights up hop by hop: shopping_agent → merchant → credentials → processor.", path: "frontend/src/views/OperatorView.jsx", repo: "ep03" },
+ { id: "sse3", x: 700, y: 216, w: 260, h: 48, label: "sseHub.js (replay)", sub: "same bus as EP 02", fill: "#13261f",
+ kicker: "Shared DNA", body: "Copied forward. History replay so a late OperatorView still sees the mandate cards. Also fans out to the merchant-site channel.", path: "backend/src/sseHub.js", repo: "ep03" },
+ { id: "bpmn3", x: 40, y: 300, w: 280, h: 52, label: "consumer-payment.bpmn", sub: "monitor → close → policy → verify", fill: "#161618",
+ kicker: "Process", body: "Service tasks: setup, start monitoring, close mandate, emit policy UI, merchant verify, issue credentials, complete checkout. Plus the breach path.", path: "bpmn/consumer-payment.bpmn", repo: "ep03" },
+ { id: "stand", x: 340, y: 300, w: 280, h: 52, label: "standing-purchase.bpmn", sub: "epilogue path", fill: "#161618",
+ kicker: "Process", body: "The $7,600 standing purchase that exceeds the $7,000 cap. Auto-decline, MandateBreachCard.", path: "bpmn/standing-purchase.bpmn", repo: "ep03" },
+ { id: "dmn3", x: 640, y: 300, w: 320, h: 52, label: "spending-policy.dmn", sub: "5 rules, first-hit", fill: "#2a2114",
+ kicker: "Policy", body: "Native Camunda DMN. Result linked to the process instance, visible in Operate. Approves the golden path, escalates HITL near the cap, declines a breach. Does not catch a tampered token — that’s the merchant’s job.", path: "dmn/spending-policy.dmn", repo: "ep03" },
+ { id: "g", x: 40, y: 384, w: 220, h: 48, label: "Golden", sub: "$6,300 auto", fill: "#13261f",
+ kicker: "Path", body: "Price drops to $3,150. Agent signs closed mandate. DMN approves. Merchant verifies 3 ✓. Autonomous checkout.", path: "CLICKPATH.md", repo: "ep03" },
+ { id: "t", x: 280, y: 384, w: 220, h: 48, label: "Tamper", sub: "crypto refuses", fill: "#2a140f",
+ kicker: "Path", body: "Cap altered inside the signed token. Agent proceeds. DMN passes. Merchant ES256 check fails. No credentials issued. The point of the episode.", path: "backend/src/mandateService.js", repo: "ep03" },
+ { id: "h", x: 520, y: 384, w: 220, h: 48, label: "HITL", sub: "98% of cap", fill: "#161618",
+ kicker: "Path", body: "Price $3,430, cart $6,860. DMN requires human sign-off at the Payment Desk, then verified checkout.", path: "frontend/src/views/PaymentDeskView.jsx", repo: "ep03" },
+ { id: "e", x: 760, y: 384, w: 200, h: 48, label: "Epilogue", sub: "breach $7,600", fill: "#161618",
+ kicker: "Path", body: "Standing purchase exceeds the cap. Auto-decline. MandateBreachCard. The mandate still holds after the happy path.", path: "bpmn/standing-purchase.bpmn", repo: "ep03" },
+ ],
+ shared: [
+ { id: "s-sse", x: 80, y: 40, w: 400, h: 70, label: "sseHub.js", sub: "EP 02 → EP 03 · emit + history replay", fill: "#2a2114",
+ kicker: "Copied forward", body: "The AG-UI bus. Keyed channels, 500-event replay, global fan-out. EP 03 adds a merchant-site channel so TicketVault can be driven as a fourth surface. If you only read one file in both repos, read this one.", path: "backend/src/sseHub.js", repo: "ep02" },
+ { id: "s-agui", x: 520, y: 40, w: 400, h: 70, label: "agui-client.js", sub: "EventSource → reducer → views", fill: "#2a2114",
+ kicker: "Copied forward", body: "No agent-UI framework. ~150 lines. Views are functions of the stream. EP 03 extends the reducer with mandate cards and PARTY_HOP.", path: "frontend/src/agui-client.js", repo: "ep02" },
+ { id: "s-work", x: 80, y: 140, w: 400, h: 70, label: "workers/index.js", sub: "Zeebe jobs that emit UI", fill: "#161618",
+ kicker: "Pattern", body: "Every service task is a worker that does a deterministic thing, then emit()s an AG-UI event. The LLM does not paint the DOM. The process does not paint the DOM. The worker translates a job into a card.", path: "backend/src/workers/index.js", repo: "ep02" },
+ { id: "s-poll", x: 520, y: 140, w: 400, h: 70, label: "taskPoller + Tasklist", sub: "HITL as a user task", fill: "#161618",
+ kicker: "Pattern", body: "User tasks are Camunda’s, not a homemade modal. The poller surfaces them as USER_TASK_PENDING; completing the card completes the job. EP 02 advisor desk and EP 03 payment desk are the same shape.", path: "backend/src/taskPoller.js", repo: "ep02" },
+ { id: "s-bpmn", x: 80, y: 240, w: 400, h: 70, label: "BPMN owns the loop", sub: "connector inside the process", fill: "#161618",
+ kicker: "Pattern", body: "EP 01 said the loop you can audit is the only one that ships. EP 02 put that loop inside Camunda (AI Agent Connector). EP 03 put payment inside the same engine so Operate shows every hop.", path: "bpmn/", repo: "ep02" },
+ { id: "s-dmn", x: 520, y: 240, w: 400, h: 70, label: "DMN owns the fork", sub: "humans when the table says so", fill: "#161618",
+ kicker: "Pattern", body: "Routing (wealth) and spending policy are tables, not if-statements in the agent. First-hit, linked to the process instance, visible in Operate. The agent does not get a vote on whether a human is required.", path: "dmn/ · bpmn/*.dmn", repo: "ep03" },
+ { id: "s-ops", x: 80, y: 340, w: 400, h: 70, label: "OperatorView", sub: "the audit surface in the film", fill: "#13261f",
+ kicker: "Pattern", body: "Always a raw-wire view next to the pretty one. If the card appeared, the event existed. That is the series’ honesty contract.", path: "frontend/src/views/OperatorView.jsx", repo: "ep02" },
+ { id: "s-shadow", x: 520, y: 340, w: 400, h: 70, label: "Runnable without the pitch", sub: "shadow mode · MIT · public", fill: "#13261f",
+ kicker: "Contract", body: "EP 02 runs with zero API keys. EP 03 is fully runnable with a free Camunda cluster. Specs and field guides live beside the code. The films are the trailer; the repos are the product.", path: "README.md", repo: "ep02" },
+ ],
+};
+
+const EDGES = {
+ stack: [
+ ["humans", "agui"], ["agui", "loop"], ["loop", "bff"], ["loop", "camunda"],
+ ["bff", "ground"], ["camunda", "auth"], ["ground", "policy"], ["auth", "policy"],
+ ],
+ ep01: [
+ ["goal", "spec"], ["goal", "captions"], ["goal", "dive"], ["goal", "narr"],
+ ["spec", "shots"], ["captions", "out"],
+ ["captions", "l0"], ["l0", "l1"], ["l1", "l2"], ["l2", "l3"],
+ ],
+ ep02: [
+ ["cust", "agui2"], ["adv", "agui2"], ["ops", "agui2"], ["cards", "agui2"],
+ ["agui2", "sse2"],
+ ["sse2", "intake"], ["sse2", "ident"], ["sse2", "risk"], ["sse2", "kb"], ["sse2", "chat"],
+ ["intake", "onb"], ["ident", "onb"], ["risk", "onb"], ["onb", "dmn2"],
+ ["chat", "conv"], ["kb", "seed"],
+ ],
+ ep03: [
+ ["shop", "openm"], ["openm", "closed"], ["closed", "verify"], ["verify", "merch"],
+ ["shop", "cust3"], ["merch", "ops3"], ["cred", "proc"],
+ ["cust3", "sse3"], ["desk3", "sse3"], ["ops3", "sse3"],
+ ["sse3", "bpmn3"], ["bpmn3", "dmn3"], ["stand", "e"],
+ ["dmn3", "g"], ["dmn3", "t"], ["dmn3", "h"],
+ ],
+ shared: [
+ ["s-sse", "s-agui"], ["s-sse", "s-work"], ["s-agui", "s-ops"],
+ ["s-work", "s-bpmn"], ["s-poll", "s-dmn"], ["s-bpmn", "s-ops"],
+ ],
+};
+
+function el(tag, attrs, children) {
+ const n = document.createElementNS("http://www.w3.org/2000/svg", tag);
+ Object.entries(attrs || {}).forEach(([k, v]) => n.setAttribute(k, v));
+ (children || []).forEach((c) => n.appendChild(typeof c === "string" ? document.createTextNode(c) : c));
+ return n;
+}
+
+function center(node) {
+ return { x: node.x + node.w / 2, y: node.y + node.h / 2 };
+}
+
+function renderView(viewId, selectedId) {
+ const stage = document.getElementById("cmap-stage");
+ const nodes = NODES[viewId];
+ const edges = EDGES[viewId] || [];
+ const byId = Object.fromEntries(nodes.map((n) => [n.id, n]));
+ const svg = el("svg", {
+ viewBox: "0 0 1000 520",
+ role: "img",
+ "aria-label": VIEWS[viewId].title,
+ });
+
+ const defs = el("defs", {}, [
+ el("marker", { id: "arrow", viewBox: "0 0 10 10", refX: "8", refY: "5", markerWidth: "7", markerHeight: "7", orient: "auto-start-reverse" }, [
+ el("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "currentColor" }),
+ ]),
+ ]);
+ svg.appendChild(defs);
+
+ edges.forEach(([a, b]) => {
+ const na = byId[a], nb = byId[b];
+ if (!na || !nb) return;
+ const ca = center(na), cb = center(nb);
+ const hi = selectedId && (selectedId === a || selectedId === b);
+ svg.appendChild(el("path", {
+ class: hi ? "edge hi" : "edge",
+ d: `M ${ca.x} ${ca.y} C ${ca.x} ${(ca.y + cb.y) / 2}, ${cb.x} ${(ca.y + cb.y) / 2}, ${cb.x} ${cb.y}`,
+ "marker-end": "url(#arrow)",
+ }));
+ });
+
+ nodes.forEach((node) => {
+ const g = el("g", {
+ class: "cmap-node" + (selectedId === node.id ? " is-active" : ""),
+ tabindex: "0",
+ role: "button",
+ "data-id": node.id,
+ "aria-label": node.label,
+ });
+ const kind =
+ node.fill === "#13261f" ? "is-surface" :
+ (node.fill === "#2a2114" || node.fill === "#2a140f" || node.fill === "#1f1c14") ? "is-check" :
+ "is-base";
+ g.classList.add(kind);
+ g.appendChild(el("rect", {
+ x: node.x, y: node.y, width: node.w, height: node.h, rx: "10",
+ "stroke-width": selectedId === node.id ? "2" : "1",
+ }));
+ g.appendChild(el("text", {
+ class: "n-label",
+ x: node.x + 16, y: node.y + (node.sub ? node.h / 2 - 4 : node.h / 2 + 5),
+ "font-size": "13", "font-weight": "600",
+ "font-family": '"Inter Variable", system-ui, sans-serif',
+ }, [node.label]));
+ if (node.sub) {
+ g.appendChild(el("text", {
+ class: "n-sub",
+ x: node.x + 16, y: node.y + node.h / 2 + 14,
+ "font-size": "11",
+ "font-family": '"JetBrains Mono Variable", ui-monospace, monospace',
+ }, [node.sub]));
+ }
+ g.addEventListener("click", (e) => {
+ e.stopPropagation();
+ selectNode(viewId, node.id);
+ });
+ g.addEventListener("keydown", (e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ selectNode(viewId, node.id);
+ }
+ });
+ svg.appendChild(g);
+ });
+
+ stage.replaceChildren(svg);
+}
+
+function inspect(node) {
+ const box = document.getElementById("inspector");
+ if (!node) {
+ box.innerHTML = `
+
Inspector
+ Click a node
+ Every box is a real file, protocol, or demo path from the public repos. Edges are runtime or verification flow, not folder nesting.
+ Keyboard: Tab through nodes, Enter to inspect.
`;
+ return;
+ }
+ const repoUrl = REPOS[node.repo];
+ const fileUrl = node.path && repoUrl
+ ? `${repoUrl}/blob/main/${node.path.split(" ")[0].replace(/\/$/, "")}`
+ : repoUrl;
+ box.innerHTML = `
+ ${node.kicker || "Node"}
+ ${node.label}
+ ${node.body}
+ ${node.path ? `${node.path}
` : ""}
+
+ ${fileUrl ? `
Open on GitHub ` : ""}
+ ${node.repo ? `
${node.repo} ` : ""}
+
`;
+}
+
+let currentView = "stack";
+let currentNode = null;
+
+function selectNode(viewId, nodeId) {
+ currentNode = NODES[viewId].find((n) => n.id === nodeId) || null;
+ renderView(viewId, nodeId);
+ inspect(currentNode);
+}
+
+function setView(viewId, nodeId) {
+ if (!VIEWS[viewId]) viewId = "stack";
+ currentView = viewId;
+ document.querySelectorAll(".view-tabs button").forEach((b) => {
+ b.setAttribute("aria-selected", String(b.dataset.view === viewId));
+ });
+ const meta = VIEWS[viewId];
+ document.getElementById("cmap-title").textContent = meta.title;
+ document.getElementById("cmap-sub").textContent = meta.subtitle;
+ const first = nodeId || null;
+ currentNode = first ? NODES[viewId].find((n) => n.id === first) : null;
+ renderView(viewId, first);
+ inspect(currentNode);
+ history.replaceState(null, "", "#" + viewId);
+}
+
+function init() {
+ document.querySelectorAll(".view-tabs button").forEach((b) => {
+ b.addEventListener("click", () => setView(b.dataset.view));
+ });
+ document.getElementById("cmap-stage").addEventListener("click", () => {
+ currentNode = null;
+ renderView(currentView, null);
+ inspect(null);
+ });
+ const hash = (location.hash || "#stack").slice(1).toLowerCase();
+ setView(["stack", "ep01", "ep02", "ep03", "shared"].includes(hash) ? hash : "stack");
+}
+
+document.addEventListener("DOMContentLoaded", init);
+window.addEventListener("hashchange", () => {
+ const hash = (location.hash || "#stack").slice(1).toLowerCase();
+ if (hash !== currentView && VIEWS[hash]) setView(hash);
+});
diff --git a/docs/assets/favicon.svg b/docs/assets/favicon.svg
new file mode 100644
index 0000000..f866c49
--- /dev/null
+++ b/docs/assets/favicon.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/docs/assets/site.css b/docs/assets/site.css
new file mode 100644
index 0000000..42e256a
--- /dev/null
+++ b/docs/assets/site.css
@@ -0,0 +1,445 @@
+/* Companion hub for Agents, Orchestrated.
+ Tokens, type, and chrome match zishanalikhan.com (field guides + series). */
+
+@font-face {
+ font-family: "Inter Variable";
+ font-style: normal;
+ font-display: swap;
+ font-weight: 100 900;
+ src: url("/_astro/inter-latin-ext-wght-normal.DO1Apj_S.woff2") format("woff2-variations");
+ unicode-range: U+100-2BA, U+2BD-2C5, U+2C7-2CC, U+2CE-2D7, U+2DD-2FF, U+304, U+308, U+329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+@font-face {
+ font-family: "Inter Variable";
+ font-style: normal;
+ font-display: swap;
+ font-weight: 100 900;
+ src: url("/_astro/inter-latin-wght-normal.Dx4kXJAl.woff2") format("woff2-variations");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: "Newsreader Variable";
+ font-style: normal;
+ font-display: swap;
+ font-weight: 200 800;
+ src: url("/_astro/newsreader-latin-ext-wght-normal.C-3rgBeH.woff2") format("woff2-variations");
+ unicode-range: U+100-2BA, U+2BD-2C5, U+2C7-2CC, U+2CE-2D7, U+2DD-2FF, U+304, U+308, U+329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+@font-face {
+ font-family: "Newsreader Variable";
+ font-style: normal;
+ font-display: swap;
+ font-weight: 200 800;
+ src: url("/_astro/newsreader-latin-wght-normal.CCVVNp6i.woff2") format("woff2-variations");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: "JetBrains Mono Variable";
+ font-style: normal;
+ font-display: swap;
+ font-weight: 100 800;
+ src: url("/_astro/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2") format("woff2-variations");
+ unicode-range: U+100-2BA, U+2BD-2C5, U+2C7-2CC, U+2CE-2D7, U+2DD-2FF, U+304, U+308, U+329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+@font-face {
+ font-family: "JetBrains Mono Variable";
+ font-style: normal;
+ font-display: swap;
+ font-weight: 100 800;
+ src: url("/_astro/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2") format("woff2-variations");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+:root {
+ --font-sans: "Inter Variable", system-ui, sans-serif;
+ --font-serif: "Newsreader Variable", Georgia, serif;
+ --font-mono: "JetBrains Mono Variable", ui-monospace, "SF Mono", monospace;
+ --bg: #fcfcfa;
+ --bg-raised: #f6f6f2;
+ --bg-inset: #f1f1ec;
+ --ink: #16191d;
+ --ink-secondary: #40474f;
+ --ink-muted: #6e7681;
+ --border: #e4e4dd;
+ --border-strong: #d0d0c8;
+ --accent: #1a7f37;
+ --accent-strong: #116329;
+ --accent-soft: #eaf7ee;
+ --danger: #cf222e;
+ --amber: #9a6700;
+ --selection: #d2f0da;
+ --max: 48rem;
+ --max-wide: 72rem;
+}
+
+html.dark {
+ --bg: #0b0f14;
+ --bg-raised: #11161d;
+ --bg-inset: #161d26;
+ --ink: #e7edf3;
+ --ink-secondary: #adb8c3;
+ --ink-muted: #7d8894;
+ --border: #1d2631;
+ --border-strong: #2b3644;
+ --accent: #3fb950;
+ --accent-strong: #56d364;
+ --accent-soft: #12261a;
+ --danger: #f85149;
+ --amber: #d29922;
+ --selection: #1c3a26;
+}
+
+*, *::before, *::after { box-sizing: border-box; }
+html { scroll-behavior: smooth; }
+html, body { margin: 0; padding: 0; }
+body {
+ font-family: var(--font-sans);
+ font-feature-settings: "ss03", "cv02";
+ background: var(--bg);
+ color: var(--ink);
+ line-height: 1.625;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+}
+::selection { background: var(--selection); }
+img { max-width: 100%; display: block; }
+a { color: inherit; text-decoration: none; }
+button { font-family: inherit; background: none; border: 0; color: inherit; cursor: pointer; }
+code, .mono, kbd { font-family: var(--font-mono); }
+
+.skip {
+ position: absolute; left: -999px; top: 0;
+ background: var(--accent); color: #fff; padding: 8px 12px; z-index: 100;
+}
+.skip:focus { left: 12px; top: 12px; }
+
+.shell {
+ width: 100%;
+ max-width: var(--max);
+ margin-left: auto;
+ margin-right: auto;
+ padding-left: 24px;
+ padding-right: 24px;
+}
+.wide-shell {
+ width: 100%;
+ max-width: var(--max-wide);
+ margin-left: auto;
+ margin-right: auto;
+ padding-left: 24px;
+ padding-right: 24px;
+}
+
+/* ── Nav (same chrome as zishanalikhan.com) ───────── */
+.site-header { padding: 32px 0 16px; }
+.site-nav {
+ display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
+ gap: 8px 16px;
+}
+.brand {
+ font-family: var(--font-mono); font-size: 13px; letter-spacing: -0.025em;
+}
+.brand .muted { color: var(--ink-muted); }
+.brand .accent { color: var(--accent); }
+.brand:hover { color: var(--accent); }
+.nav-right { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; }
+.nav-right > a, .icon-btn {
+ padding: 4px 8px; font-size: 13px; border-radius: 6px;
+ color: var(--ink-muted); transition: color 0.12s, background 0.12s;
+}
+.nav-right > a:hover, .icon-btn:hover { color: var(--ink); }
+.nav-right > a.here { color: var(--ink); font-weight: 500; }
+.icon-btn { display: inline-flex; align-items: center; padding: 6px; }
+html:not(.dark) .icon-sun { display: none; }
+html.dark .icon-moon { display: none; }
+
+.locale {
+ display: flex; align-items: center; padding: 0 4px;
+ font-family: var(--font-mono); font-size: 11px; color: var(--ink-muted);
+}
+.locale a {
+ padding: 0 4px; color: var(--ink-muted); transition: color 0.12s;
+}
+.locale a:hover { color: var(--ink); }
+.locale a.here { color: var(--ink); font-weight: 500; }
+.locale .dot { user-select: none; }
+
+/* ── Type ─────────────────────────────────────────── */
+.kicker {
+ font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.14em;
+ text-transform: uppercase; color: var(--ink-muted); margin-bottom: 12px;
+}
+h1, h2, h3 { letter-spacing: -0.025em; line-height: 1.15; margin: 0; }
+h1 { font-family: var(--font-serif); font-size: 2.25rem; font-weight: 600; }
+h2 { font-family: var(--font-serif); font-size: 1.35rem; font-weight: 600; }
+h3 { font-size: 1.125rem; font-weight: 600; }
+.lede {
+ margin-top: 16px; max-width: 36rem;
+ font-size: 15px; color: var(--ink-secondary); line-height: 1.625;
+}
+.pullquote {
+ margin: 24px 0 0; max-width: 36rem; padding: 16px;
+ border: 1px solid var(--border); border-left: 2px solid var(--accent);
+ border-radius: 6px; background: var(--bg-raised);
+}
+.pullquote p {
+ margin: 0; font-family: var(--font-serif); font-style: italic;
+ font-size: 16px; color: var(--ink-secondary);
+}
+
+.prose-link {
+ color: var(--ink-secondary);
+ text-decoration: underline;
+ text-decoration-color: var(--accent);
+ text-underline-offset: 3px;
+ text-decoration-thickness: 1px;
+}
+.prose-link:hover { color: var(--accent); }
+
+/* ── Hero / sections ──────────────────────────────── */
+main { flex: 1; }
+.hero { padding: 64px 0 40px; }
+.section { padding: 8px 0 48px; }
+.section-head { margin-bottom: 20px; }
+.section-head p { margin: 8px 0 0; font-size: 14px; color: var(--ink-muted); }
+
+.note {
+ margin-top: 20px; font-size: 13.5px; color: var(--ink-secondary);
+ padding: 12px 14px; border: 1px dashed var(--border); border-radius: 8px;
+ background: var(--bg-raised);
+}
+.note a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
+
+/* ── Buttons (match Start here / Field guides) ───── */
+.btn-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; align-items: center; }
+.btn {
+ display: inline-flex; align-items: center; gap: 6px;
+ padding: 8px 16px; border-radius: 6px; font-size: 13px; font-weight: 500;
+ border: 1px solid var(--border); background: transparent;
+ color: var(--ink-secondary);
+ transition: border-color 0.12s, color 0.12s, background 0.12s;
+}
+.btn:hover { border-color: var(--border-strong); color: var(--ink); }
+.btn-accent {
+ background: var(--ink); color: var(--bg); border-color: transparent;
+}
+.btn-accent:hover { background: var(--accent); color: #fff; border-color: transparent; }
+
+/* ── Episode list (matches series index cards) ────── */
+.card-list { display: flex; flex-direction: column; gap: 12px; }
+.card {
+ display: block; padding: 20px;
+ border: 1px solid var(--border); border-radius: 8px;
+ background: var(--bg-raised); transition: border-color 0.15s;
+}
+a.card:hover { border-color: var(--border-strong); }
+a.card:hover h2 { color: var(--accent); }
+.card.queued {
+ border-style: dashed; opacity: 0.7;
+}
+.card-meta {
+ display: flex; align-items: center; gap: 12px; margin-bottom: 8px;
+ font-family: var(--font-mono); font-size: 11px; color: var(--ink-muted);
+}
+.card-meta .ok { color: var(--accent); }
+.card-meta .wip { color: var(--amber); }
+.card-meta .when { margin-left: auto; }
+.card h2 { font-size: 20px; }
+.card .line {
+ margin: 6px 0 0; font-family: var(--font-serif); font-style: italic;
+ font-size: 13.5px; color: var(--ink-muted);
+}
+.card p.body { margin: 10px 0 0; font-size: 13.5px; color: var(--ink-secondary); max-width: 42rem; }
+.card-links { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 14px; font-size: 13px; }
+.card-links a { color: var(--ink-muted); }
+.card-links a:hover { color: var(--accent); }
+
+.shot-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 14px; }
+.shot {
+ position: relative; border-radius: 8px; overflow: hidden;
+ border: 1px solid var(--border); background: var(--bg-inset); aspect-ratio: 16/10;
+}
+.shot img { width: 100%; height: 100%; object-fit: cover; }
+.shot span {
+ position: absolute; left: 8px; bottom: 8px;
+ font-family: var(--font-mono); font-size: 10px; color: #fff;
+ background: rgba(0,0,0,0.5); padding: 2px 7px; border-radius: 4px;
+}
+
+/* ── Stack / protocols ────────────────────────────── */
+.stack { display: flex; flex-direction: column; gap: 8px; }
+.stack-row {
+ display: grid; grid-template-columns: 92px 1fr auto; gap: 12px; align-items: start;
+ padding: 14px 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-raised);
+}
+.stack-row .layer {
+ font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.08em;
+ text-transform: uppercase; color: var(--ink-muted); padding-top: 3px;
+}
+.stack-row strong { font-size: 14px; }
+.stack-row span { display: block; font-size: 13px; color: var(--ink-muted); margin-top: 2px; }
+.pill {
+ font-family: var(--font-mono); font-size: 10.5px; white-space: nowrap;
+ padding: 3px 7px; border-radius: 4px; border: 1px solid var(--border); color: var(--ink-muted);
+}
+
+.protocols { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 16px; }
+.proto {
+ padding: 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-raised);
+}
+.proto .name { font-family: var(--font-mono); font-size: 12px; font-weight: 500; }
+.proto p { margin: 6px 0 0; font-size: 13px; color: var(--ink-muted); }
+
+/* ── Codemap ──────────────────────────────────────── */
+.cmap-hero { padding: 48px 0 8px; }
+.view-tabs {
+ display: flex; flex-wrap: wrap; gap: 4px;
+ padding: 4px; margin: 24px 0 16px;
+ border: 1px solid var(--border); border-radius: 8px; background: var(--bg-raised);
+ width: fit-content;
+}
+.view-tabs button {
+ padding: 6px 12px; border-radius: 6px; font-size: 13px; color: var(--ink-muted);
+}
+.view-tabs button[aria-selected="true"] {
+ background: var(--bg); color: var(--ink); font-weight: 500;
+ box-shadow: 0 0 0 1px var(--border);
+}
+.cmap-shell {
+ display: grid; grid-template-columns: 1fr 300px; gap: 12px; align-items: start;
+}
+.cmap-stage {
+ min-height: 560px; border: 1px solid var(--border); border-radius: 12px;
+ background: var(--bg-raised); overflow: hidden;
+}
+.cmap-stage svg { display: block; width: 100%; height: auto; color: var(--border-strong); }
+.inspector {
+ position: sticky; top: 16px;
+ padding: 18px; border: 1px solid var(--border); border-radius: 12px; background: var(--bg-raised);
+}
+.inspector .kicker { margin-bottom: 6px; }
+.inspector h3 { font-family: var(--font-serif); font-size: 18px; margin-bottom: 8px; }
+.inspector p { margin: 0 0 10px; font-size: 13.5px; color: var(--ink-secondary); }
+.inspector .path {
+ font-family: var(--font-mono); font-size: 11px; color: var(--ink-muted);
+ background: var(--bg); border: 1px solid var(--border);
+ border-radius: 6px; padding: 8px 10px; margin: 10px 0; word-break: break-all;
+}
+.inspector .links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
+.node-chip {
+ font-family: var(--font-mono); font-size: 11px; padding: 3px 7px;
+ border-radius: 4px; border: 1px solid var(--border); color: var(--ink-muted);
+}
+.cmap-node { cursor: pointer; }
+.cmap-node rect { fill: var(--bg); stroke: var(--border); }
+.cmap-node.is-surface rect { fill: var(--accent-soft); }
+.cmap-node.is-check rect { fill: var(--accent-soft); stroke: var(--accent); }
+.cmap-node.is-active rect { stroke: var(--accent); stroke-width: 2; }
+.cmap-node .n-label { fill: var(--ink); }
+.cmap-node .n-sub { fill: var(--ink-muted); }
+.edge { fill: none; stroke: var(--border-strong); stroke-width: 1.25; }
+.edge.hi { stroke: var(--accent); stroke-width: 1.8; }
+.legend { display: flex; flex-wrap: wrap; gap: 14px; font-size: 12px; color: var(--ink-muted); margin: 0 0 16px; }
+.legend i { display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: 5px; vertical-align: -1px; }
+
+.vocab { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
+.vocab-item { padding: 14px 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-raised); }
+.vocab-item code { color: var(--accent); font-size: 12px; }
+.vocab-item p { margin: 6px 0 0; font-size: 13px; color: var(--ink-muted); }
+
+.shared-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
+.shared-card { padding: 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-raised); }
+.shared-card h3 { font-size: 15px; margin-bottom: 6px; }
+.shared-card p { margin: 0; font-size: 13px; color: var(--ink-muted); }
+
+.axes {
+ position: relative; background: var(--bg-raised);
+ border: 1px solid var(--border); border-radius: 12px;
+ padding: 40px 20px 32px 48px; min-height: 340px;
+}
+.axes-grid { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 8px; height: 260px; }
+.quad { border: 1px dashed var(--border); border-radius: 8px; padding: 12px; }
+.quad.hot { border-color: var(--accent); background: var(--accent-soft); }
+.dot {
+ display: inline-flex; margin: 4px 6px 0 0; padding: 3px 8px;
+ font-size: 12px; border-radius: 999px; border: 1px solid var(--border); background: var(--bg);
+}
+.axis-x, .axis-y {
+ position: absolute; font-family: var(--font-mono); font-size: 10px;
+ letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-muted);
+}
+.axis-x { bottom: 8px; left: 48px; right: 20px; display: flex; justify-content: space-between; }
+.axis-y {
+ top: 40px; bottom: 32px; left: 8px; writing-mode: vertical-rl;
+ transform: rotate(180deg); display: flex; justify-content: space-between;
+}
+
+/* ── Footer ───────────────────────────────────────── */
+.site-footer {
+ padding: 48px 0 56px;
+ margin-top: 80px;
+ border-top: 1px solid var(--border);
+}
+.foot-row { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 16px; }
+.site-footer p, .site-footer a { font-size: 13px; color: var(--ink-muted); }
+.site-footer a:hover { color: var(--ink); }
+.foot-links { display: flex; flex-wrap: wrap; gap: 16px; }
+.fine { margin-top: 16px; font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted); opacity: 0.7; }
+
+/* ── Search (same modal as the personal site) ─────── */
+#search-modal {
+ background: transparent; border: 0; padding: 0;
+ width: min(36rem, calc(100vw - 32px)); margin: 12vh auto 0;
+}
+#search-modal::backdrop { background: rgba(0, 0, 0, 0.6); }
+.search-box {
+ border-radius: 12px; border: 1px solid var(--border-strong);
+ background: var(--bg); overflow: hidden;
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28);
+}
+.search-row {
+ display: flex; align-items: center; gap: 12px;
+ padding: 0 16px; border-bottom: 1px solid var(--border);
+}
+.search-row .prompt { font-family: var(--font-mono); font-size: 13px; color: var(--accent); }
+#search-input {
+ flex: 1; padding: 14px 0; border: 0; background: transparent;
+ font: 15px/1.4 var(--font-sans); color: var(--ink); outline: none;
+}
+#search-input::placeholder { color: var(--ink-muted); }
+.search-row kbd {
+ font-size: 10.5px; padding: 2px 6px; border-radius: 4px;
+ border: 1px solid var(--border); color: var(--ink-muted);
+}
+#search-results { max-height: 50vh; overflow-y: auto; }
+.search-hit {
+ display: block; padding: 12px 16px;
+ border-bottom: 1px solid var(--border);
+}
+.search-hit:last-child { border-bottom: 0; }
+.search-hit:hover { background: var(--bg-raised); }
+.search-hit .t { font-size: 14px; font-weight: 500; }
+.search-hit .k {
+ font-family: var(--font-mono); font-size: 10px; text-transform: uppercase;
+ letter-spacing: 0.06em; color: var(--accent); margin-left: 8px;
+}
+.search-hit p { margin: 4px 0 0; font-size: 12.5px; color: var(--ink-muted); }
+.search-msg { padding: 16px; font-size: 13px; color: var(--ink-muted); font-family: var(--font-mono); }
+
+@media (max-width: 900px) {
+ .cmap-shell, .shot-row, .vocab, .shared-grid, .protocols { grid-template-columns: 1fr; }
+ .stack-row { grid-template-columns: 1fr; gap: 4px; }
+ .inspector { position: static; }
+}
+@media (max-width: 560px) {
+ .shell, .wide-shell { padding-left: 16px; padding-right: 16px; }
+ .hero { padding-top: 36px; }
+ h1 { font-size: 1.875rem; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html { scroll-behavior: auto; }
+}
diff --git a/docs/assets/site.js b/docs/assets/site.js
new file mode 100644
index 0000000..dc8f140
--- /dev/null
+++ b/docs/assets/site.js
@@ -0,0 +1,74 @@
+(function () {
+ const root = document.documentElement;
+ const stored = localStorage.getItem("theme");
+ const wantsLight = stored === "light";
+ root.classList.toggle("dark", !wantsLight);
+
+ document.getElementById("theme-toggle")?.addEventListener("click", () => {
+ const isDark = root.classList.toggle("dark");
+ localStorage.setItem("theme", isDark ? "dark" : "light");
+ });
+
+ document.querySelectorAll("#locale-picker a[data-locale]").forEach((link) => {
+ link.addEventListener("click", () => {
+ localStorage.setItem("locale", link.dataset.locale);
+ });
+ });
+
+ const modal = document.getElementById("search-modal");
+ const input = document.getElementById("search-input");
+ const resultsEl = document.getElementById("search-results");
+ let pagefind = null;
+
+ async function openSearch() {
+ if (!modal || modal.open) return;
+ modal.showModal();
+ input?.focus();
+ if (!pagefind) {
+ try {
+ pagefind = await import("/pagefind/pagefind.js");
+ pagefind.init();
+ } catch {
+ if (resultsEl) {
+ resultsEl.innerHTML =
+ `${resultsEl.dataset.msgNobuild || "Search index not built yet."}
`;
+ }
+ }
+ }
+ }
+
+ document.addEventListener("keydown", (e) => {
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
+ e.preventDefault();
+ modal?.open ? modal.close() : openSearch();
+ }
+ });
+ document.getElementById("search-open")?.addEventListener("click", openSearch);
+ modal?.addEventListener("click", (e) => {
+ if (e.target === modal) modal.close();
+ });
+
+ input?.addEventListener("input", async () => {
+ if (!pagefind || !resultsEl) return;
+ const query = input.value.trim();
+ if (!query) {
+ resultsEl.innerHTML = "";
+ return;
+ }
+ const search = await pagefind.debouncedSearch(query);
+ if (!search) return;
+ const items = await Promise.all(search.results.slice(0, 8).map((r) => r.data()));
+ resultsEl.innerHTML = items.length
+ ? items
+ .map(
+ (item) => `
+
+ ${item.meta.title ?? item.url}
+ ${item.meta.kind ? `${item.meta.kind} ` : ""}
+ ${item.excerpt}
+ `
+ )
+ .join("")
+ : `${resultsEl.dataset.msgEmpty || "No matches."}
`;
+ });
+})();
diff --git a/docs/codemap.html b/docs/codemap.html
new file mode 100644
index 0000000..f2ff4c6
--- /dev/null
+++ b/docs/codemap.html
@@ -0,0 +1,182 @@
+
+
+
+
+
+ Codemap · Agents, Orchestrated
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Skip to content
+
+
+
+
+ For the extra curious
+ Visual codemap
+ The three public repos as one picture. Nodes are files, protocols, or demo paths. Edges are runtime flow — events, jobs, signatures — not folder nesting.
+
+
+
+
+ Series stack
+ EP 01 loops
+ EP 02 AURUM
+ EP 03 AP2
+ Shared DNA
+
+
The series as one system
+
+
+ green = the check / the signature / a human surface
+ raised = process, worker, spec
+
+
+
+
+
+
+
Two questions define any loop.
+
Continuation on the x-axis, locus on the y-axis. The interesting differences sit on the right: how “done” is decided.
+
+
+
cloud-persistent in-session
+
+
+ trigger · cloud
+ Claude Routines
+ Antigravity /schedule
+ Cursor Automations
+
+
+ verification · cloud
+ Codex Goal mode
+ Antigravity /goal
+
+
+ trigger · session
+ Claude /loop
+
+
+ verification · session
+ Claude Code /goal
+
+
+
gated by a trigger gated by verification
+
+
+
+
+
+
Event vocabulary
+
Everything the UI does is a typed event on one SSE stream.
+
+
+
TEXT_MESSAGE_*Streaming concierge copy. Start / content delta / end.
+
TOOL_CALL_*An agent step with args and results. OperatorView lives on these.
+
GENERATIVE_UIA real component: component + props.
+
USER_TASK_PENDINGA Camunda user task, surfaced as a card.
+
STEP_STARTED / FINISHEDProcess progress on customer stepper and operator log.
+
PARTY_HOPEP 03. Lights the four-party AP2 strip.
+
+
+
+
+
+
How to read a PR in these repos
+
Start at the wire, not the model.
+
+
+
1. The event If a card appeared, an event was emitted. Find emit( in the worker.
+
2. The job The worker is a Zeebe job. Operate shows the same sequence.
+
3. The fork If a human appeared, a DMN table or a user task put them there.
+
4. The proof EP 01: frame stills. EP 02: operator log. EP 03: merchant signature check.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..3c7f346
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,271 @@
+
+
+
+
+
+ Agents, Orchestrated · letmereviewyourcode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Skip to content
+
+
+
+
+ Series hub
+ Agents, Orchestrated
+ One layer of the agentic stack per episode. Built for real, reviewed in public. This page is the visual companion: films, repos, and a codemap. The essays stay where they are.
+
+ “Autonomy is table stakes. The moat is everything around the agent.”
+
+
+ Essays: zishanalikhan.com/series/agents-orchestrated .
+ This hub does not replace that index.
+
+
+
+
+
+
+
Episodes
+
Three shipped. Three queued. Watch, read, or open the code.
+
+
+
+
+ EP 01
+ ✓ shipped
+ Jun 2026
+
+ Loops: the agentic primitive everything converged on
+ “The loop you can audit is the only one that ships.”
+ Claude Code, Codex, and Antigravity all shipped a /goal-style command. Autonomy stopped being the differentiator. How each loop decides it’s done is the product. The film was itself a loop: one /goal pointed at a spec, then a four-layer verification pass.
+
+
+
+ the 2×2
+
+
+
+
+
+
+
+ EP 02
+ ✓ shipped
+ Jul 2026
+
+ AG-UI: the agent that streams an interface, not a transcript
+ “One process. Three models. Zero glue code.”
+ AURUM: a private bank onboarding clients on Camunda 8. Every agent decision streams as live UI over AG-UI. When the money gets serious, the process pauses for a human in Operate. Runs without API keys in shadow mode.
+
+
+
+ customer
+
+
+
+ advisor desk
+
+
+
+
+
+
+
+ EP 03
+ ✓ shipped
+ Jul 2026
+
+ Agent payments: mandates, not trust
+ “Agents that can pay are easy. Agents you can audit paying are the product.”
+ AP2 signed mandates. A $7,000 ceiling. Tampering the limit is refused by cryptography; getting close is escalated by a Camunda DMN table. Four paths: golden, tamper, HITL, breach.
+
+
+
+ customer
+
+
+
+ TicketVault
+
+
+
+
+
+
+
EP 04 ○ queued
+
Multi-LLM Polyglot
+
One process, many models. Routing decided by the orchestrator, priced per case.
+
+
+
EP 05 ○ queued
+
Agent-to-Agent
+
Agents delegating to agents. Where orchestration beats a swarm, and where it does not.
+
+
+
EP 06 ○ queued
+
Verification, Deployed
+
Agents have a shape now. Making the verification layer a deployable artifact.
+
+
+
+
+
+
+
How the layers compose
+
The process is the orchestrator. The LLM is the language layer.
+
+
+
+
Humans
+
Customer · advisor · operator · merchant Three surfaces, one stream. Operator view is the raw wire.
+
EP 02–03
+
+
+
Wire
+
AG-UI over SSE TEXT_MESSAGE, TOOL_CALL, GENERATIVE_UI, USER_TASK.
+
EP 02–03
+
+
+
Loop
+
Generate → run → check → repeat Done is evidence, not vibes.
+
EP 01
+
+
+
Control
+
Camunda 8 · BPMN + user tasks Paused tokens are the product.
+
EP 02–03
+
+
+
Grounding
+
Knowledge base as a process step What it can’t verify, it doesn’t invent.
+
EP 02
+
+
+
Authority
+
AP2 mandates · ES256 JWS You sign the rules. The agent signs the cart. The merchant verifies both.
+
EP 03
+
+
+
Policy
+
DMN decision tables Wealth routing. Spending policy. Visible in Operate.
+
EP 02–03
+
+
+
+
MCP
Connects agents to tools and context.
+
A2A
Connects agents to other agents.
+
AG-UI
Connects agents to users — the last mile.
+
AP2
Connects agents to money, with a signature.
+
+
+
+
+
+
For the extra curious
+
A visual map of the three public repos. Click a node for the file, what it emits, and the GitHub link.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/vercel.json b/docs/vercel.json
new file mode 100644
index 0000000..69567d8
--- /dev/null
+++ b/docs/vercel.json
@@ -0,0 +1,4 @@
+{
+ "cleanUrls": true,
+ "trailingSlash": true
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..15b2d60
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "agents-orchestrated-hub",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "test": "node --test tests/hub.test.mjs",
+ "stage": "node scripts/stage-prefix.mjs",
+ "test:live": "node --test tests/live-essays.test.mjs",
+ "test:parity": "node --test tests/deployed-parity.test.mjs",
+ "test:all": "node --test tests/*.test.mjs"
+ }
+}
diff --git a/scripts/deploy.mjs b/scripts/deploy.mjs
new file mode 100644
index 0000000..481305f
--- /dev/null
+++ b/scripts/deploy.mjs
@@ -0,0 +1,53 @@
+#!/usr/bin/env node
+/**
+ * Deploy this hub as its own Vercel project.
+ * Never attaches zishanalikhan.com (that would take over the personal site).
+ * After deploy, prints the rewrite to add on the personal-site project.
+ */
+import { spawnSync } from "node:child_process";
+
+const token = process.env.VERCEL_TOKEN;
+if (!token) {
+ console.error("VERCEL_TOKEN is not set");
+ process.exit(1);
+}
+
+function run(args) {
+ const r = spawnSync("npx", ["--yes", "vercel@latest", ...args], {
+ encoding: "utf8",
+ env: { ...process.env, VERCEL_TOKEN: token },
+ });
+ process.stdout.write(r.stdout || "");
+ process.stderr.write(r.stderr || "");
+ if (r.status !== 0) process.exit(r.status ?? 1);
+ return (r.stdout || "").trim();
+}
+
+const prod = process.argv.includes("--prod");
+const args = [
+ "deploy",
+ "--yes",
+ "--token",
+ token,
+ "--name",
+ "agents-orchestrated-hub",
+];
+if (prod) args.push("--prod");
+
+const url = run(args);
+console.log("\nDeployed:", url);
+console.log(`
+Add this rewrite on the zishanalikhan.com Vercel project (not this one):
+
+{
+ "source": "/agents-orchestrated",
+ "destination": "${url.replace(/\/$/, "")}/"
+}
+{
+ "source": "/agents-orchestrated/:path*",
+ "destination": "${url.replace(/\/$/, "")}/:path*"
+}
+
+Do not point the personal-site domain at this project.
+Do not rewrite /series/agents-orchestrated.
+`);
diff --git a/scripts/parent-site-rewrites.json b/scripts/parent-site-rewrites.json
new file mode 100644
index 0000000..bec4584
--- /dev/null
+++ b/scripts/parent-site-rewrites.json
@@ -0,0 +1,11 @@
+{
+ "comment": "Project routes on letmereviewyourcode-github-io. Regex so subpaths keep the prefix the hub actually serves.",
+ "rewrites": [
+ {
+ "src": "^/agents-orchestrated(/.*)?$",
+ "srcSyntax": "regex",
+ "dest": "https://agents-orchestrated-hub.vercel.app/agents-orchestrated$1",
+ "routeType": "rewrite"
+ }
+ ]
+}
diff --git a/scripts/stage-prefix.mjs b/scripts/stage-prefix.mjs
new file mode 100644
index 0000000..404fcd4
--- /dev/null
+++ b/scripts/stage-prefix.mjs
@@ -0,0 +1,19 @@
+/**
+ * Copy the hub under docs/agents-orchestrated/ so a reverse-proxy
+ * that forwards the original path still finds files.
+ */
+import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const docs = join(dirname(fileURLToPath(import.meta.url)), "..", "docs");
+const dest = join(docs, "agents-orchestrated");
+
+if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
+mkdirSync(dest, { recursive: true });
+
+for (const name of ["index.html", "codemap.html", "assets"]) {
+ cpSync(join(docs, name), join(dest, name), { recursive: true });
+}
+
+console.log("staged", dest);
diff --git a/tests/deployed-parity.test.mjs b/tests/deployed-parity.test.mjs
new file mode 100644
index 0000000..fd3740d
--- /dev/null
+++ b/tests/deployed-parity.test.mjs
@@ -0,0 +1,69 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { test } from "node:test";
+
+const root = join(dirname(fileURLToPath(import.meta.url)), "..");
+const LIVE = process.env.LIVE_HUB_URL || "https://zishanalikhan.com/agents-orchestrated/";
+const REQUIRED = process.env.REQUIRE_DEPLOYED === "1";
+
+function local() {
+ return readFileSync(join(root, "docs/index.html"), "utf8");
+}
+
+function cmap() {
+ return readFileSync(join(root, "docs/codemap.html"), "utf8");
+}
+
+async function get(url) {
+ const res = await fetch(url, { redirect: "follow" });
+ return { status: res.status, body: await res.text(), url: res.url };
+}
+
+const markers = [
+ "Agents, Orchestrated · letmereviewyourcode",
+ "visual companion",
+ "Visual codemap",
+ "Loops: the agentic primitive everything converged on",
+ "AG-UI: the agent that streams an interface, not a transcript",
+ "Agent payments: mandates, not trust",
+ "For the extra curious",
+];
+
+test("deployed hub matches local markers (or is still unpublished)", async (t) => {
+ const { status, body } = await get(LIVE);
+ if (status === 404) {
+ if (REQUIRED) {
+ assert.fail(`${LIVE} is 404; rewrite/deploy is not live`);
+ }
+ t.skip(`${LIVE} is still 404 — hub not rewritten onto the personal site yet`);
+ return;
+ }
+ assert.equal(status, 200, `${LIVE} returned ${status}`);
+ const html = local();
+ for (const s of markers) {
+ assert.ok(html.includes(s) || html.includes(s.replace(" · letmereviewyourcode", "")), `local missing ${s}`);
+ assert.ok(body.includes(s) || body.includes("Visual codemap"), `deployed missing ${s}`);
+ }
+ assert.ok(body.includes("codemap"), "deployed hub missing codemap");
+ assert.ok(!body.includes("This page doesn't exist"), "deployed hub is still the personal-site 404");
+ assert.ok(!body.includes("agents-orchestrated-hub.vercel.app"), "deployed HTML still points at the Vercel app domain");
+});
+
+test("deployed codemap matches local views (or is still unpublished)", async (t) => {
+ const base = LIVE.endsWith("/") ? LIVE : LIVE + "/";
+ const { status, body } = await get(base + "codemap/");
+ const fallback = status >= 400 ? await get(base + "codemap.html") : { status, body };
+ if (fallback.status === 404) {
+ if (REQUIRED) assert.fail("codemap 404");
+ t.skip("codemap not deployed yet");
+ return;
+ }
+ assert.equal(fallback.status, 200);
+ const html = cmap();
+ for (const view of ["Series stack", "EP 01 loops", "EP 02 AURUM", "EP 03 AP2", "Shared DNA"]) {
+ assert.ok(html.includes(view), `local codemap missing ${view}`);
+ assert.ok(fallback.body.includes(view), `deployed codemap missing ${view}`);
+ }
+});
diff --git a/tests/hub.test.mjs b/tests/hub.test.mjs
new file mode 100644
index 0000000..583c4a5
--- /dev/null
+++ b/tests/hub.test.mjs
@@ -0,0 +1,114 @@
+import assert from "node:assert/strict";
+import { readFileSync, existsSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { test } from "node:test";
+
+const root = join(dirname(fileURLToPath(import.meta.url)), "..");
+const docs = join(root, "docs");
+
+function read(rel) {
+ return readFileSync(join(docs, rel), "utf8");
+}
+
+test("hub files exist", () => {
+ for (const f of [
+ "index.html",
+ "codemap.html",
+ "assets/site.css",
+ "assets/site.js",
+ "assets/codemap.js",
+ "assets/favicon.svg",
+ ]) {
+ assert.equal(existsSync(join(docs, f)), true, `missing ${f}`);
+ }
+});
+
+test("hub is a companion, not the essay index", () => {
+ const html = read("index.html");
+ assert.match(html, /visual companion/i);
+ assert.match(html, /https:\/\/zishanalikhan\.com\/series\/agents-orchestrated\/|href="\/series\/agents-orchestrated\/"/);
+ assert.match(html, /canonical" href="https:\/\/zishanalikhan\.com\/agents-orchestrated\//);
+ assert.doesNotMatch(html, /This page doesn't exist/);
+});
+
+test("hub title and chrome match the personal site", () => {
+ const html = read("index.html");
+ const css = read("assets/site.css");
+ assert.match(html, /Agents, Orchestrated · letmereviewyourcode<\/title>/);
+ assert.match(html, /letmereview/);
+ assert.match(html, /yourcode/);
+ assert.match(html, /theme-toggle/);
+ assert.match(css, /Newsreader Variable/);
+ assert.match(css, /Inter Variable/);
+ assert.match(css, /\/_astro\/inter-latin-wght-normal/);
+ assert.doesNotMatch(html, /vercel\.app/);
+ assert.doesNotMatch(css, /fonts\.googleapis/);
+});
+
+test("shipped episodes plus queued arc are listed", () => {
+ const html = read("index.html");
+ for (const s of [
+ "Loops: the agentic primitive everything converged on",
+ "AG-UI: the agent that streams an interface, not a transcript",
+ "Agent payments: mandates, not trust",
+ "Multi-LLM Polyglot",
+ "Agent-to-Agent",
+ "Verification, Deployed",
+ "what-are-loops",
+ "camunda-agui-wealth-demo",
+ "camunda-ap2-payments-demo",
+ ]) {
+ assert.match(html, new RegExp(s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
+ }
+});
+
+test("assets use the /agents-orchestrated prefix so nested clean URLs still load", () => {
+ const index = read("index.html");
+ const cmap = read("codemap.html");
+ for (const html of [index, cmap]) {
+ assert.match(html, /href="\/agents-orchestrated\/assets\/site\.css"/);
+ assert.match(html, /src="\/agents-orchestrated\/assets\/site\.js"/);
+ assert.doesNotMatch(html, /href="assets\//);
+ assert.doesNotMatch(html, /src="assets\//);
+ }
+ assert.match(cmap, /src="\/agents-orchestrated\/assets\/codemap\.js"/);
+ assert.match(index, /href="\/agents-orchestrated\/codemap\/"/);
+});
+
+test("codemap has the five views and inspector", () => {
+ const html = read("codemap.html");
+ const js = read("assets/codemap.js");
+ for (const view of ["stack", "ep01", "ep02", "ep03", "shared"]) {
+ assert.match(html, new RegExp(`data-view="${view}"`));
+ assert.match(js, new RegExp(`${view}:`));
+ }
+ assert.match(html, /id="cmap-stage"/);
+ assert.match(html, /id="inspector"/);
+ assert.match(js, /selectNode/);
+});
+
+test("this repo does not ship the essay routes", () => {
+ assert.equal(existsSync(join(docs, "series")), false);
+ const index = read("index.html");
+ assert.match(index, /href="\/series\/agents-orchestrated\/"/);
+});
+
+test("stage-prefix copies the hub under /agents-orchestrated", async () => {
+ const { spawnSync } = await import("node:child_process");
+ const r = spawnSync(process.execPath, [join(root, "scripts/stage-prefix.mjs")], { encoding: "utf8" });
+ assert.equal(r.status, 0, r.stderr || r.stdout);
+ assert.equal(existsSync(join(docs, "agents-orchestrated/index.html")), true);
+ assert.equal(existsSync(join(docs, "agents-orchestrated/codemap.html")), true);
+ assert.equal(existsSync(join(docs, "agents-orchestrated/assets/site.css")), true);
+});
+
+test("root vercel.json serves docs and prefixes /agents-orchestrated", () => {
+ const cfg = JSON.parse(readFileSync(join(root, "vercel.json"), "utf8"));
+ assert.equal(cfg.outputDirectory, "docs");
+ assert.equal(cfg.trailingSlash, true);
+ const sources = (cfg.rewrites || []).map((r) => r.source);
+ assert.ok(sources.includes("/agents-orchestrated"));
+ assert.ok(sources.includes("/agents-orchestrated/:path*"));
+ assert.ok(!sources.some((s) => s.includes("/series/")), "must not rewrite essay routes");
+});
diff --git a/tests/live-essays.test.mjs b/tests/live-essays.test.mjs
new file mode 100644
index 0000000..17575e2
--- /dev/null
+++ b/tests/live-essays.test.mjs
@@ -0,0 +1,63 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+
+const ESSAYS = [
+ {
+ url: "https://zishanalikhan.com/series/agents-orchestrated/",
+ must: [
+ "Agents, Orchestrated",
+ "Loops: the agentic primitive everything converged on",
+ "AG-UI: the agent that streams an interface, not a transcript",
+ "Agent payments: mandates, not trust",
+ "Multi-LLM Polyglot",
+ ],
+ mustNot: ["This page doesn't exist"],
+ },
+ {
+ url: "https://zishanalikhan.com/series/agents-orchestrated/loops/",
+ must: ["All roads led to /goal", "the loop you can audit"],
+ },
+ {
+ url: "https://zishanalikhan.com/series/agents-orchestrated/ag-ui/",
+ must: ["AURUM", "One process. Three models. Zero glue code."],
+ },
+ {
+ url: "https://zishanalikhan.com/series/agents-orchestrated/agent-payments/",
+ must: ["mandates, not trust", "AP2"],
+ },
+];
+
+async function get(url) {
+ const res = await fetch(url, { redirect: "follow" });
+ const body = await res.text();
+ return { status: res.status, body };
+}
+
+test("essay index and episode pages still ship", async () => {
+ for (const page of ESSAYS) {
+ const { status, body } = await get(page.url);
+ assert.equal(status, 200, `${page.url} returned ${status}`);
+ for (const s of page.must || []) {
+ assert.ok(body.includes(s), `${page.url} missing: ${s}`);
+ }
+ for (const s of page.mustNot || []) {
+ assert.ok(!body.includes(s), `${page.url} unexpectedly contains: ${s}`);
+ }
+ }
+});
+
+test("the reserved hub path is not an essay page", async () => {
+ const { status, body } = await get("https://zishanalikhan.com/agents-orchestrated/");
+ // Before the rewrite lands this is a 404. After deploy it is the hub.
+ // Either way it must never serve the essay index HTML.
+ assert.ok(
+ !body.includes("One layer of the agentic stack per episode. Built for real, reviewed in public.") ||
+ body.includes("visual companion") ||
+ status === 404,
+ "hub path collided with the essay index"
+ );
+ if (status === 200) {
+ assert.match(body, /visual companion/i);
+ assert.match(body, /codemap/i);
+ }
+});
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..26e4987
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://openapi.vercel.sh/vercel.json",
+ "installCommand": "echo skip",
+ "buildCommand": "node --test tests/hub.test.mjs && node scripts/stage-prefix.mjs",
+ "outputDirectory": "docs",
+ "cleanUrls": true,
+ "trailingSlash": true,
+ "rewrites": [
+ { "source": "/agents-orchestrated", "destination": "/" },
+ { "source": "/agents-orchestrated/", "destination": "/" },
+ { "source": "/agents-orchestrated/:path*", "destination": "/:path*" }
+ ]
+}