Mumega

Building a Shared Knowledge Substrate for Human-Agent Teams

The session started with a broken brain. Our AI orchestration service — cortex-events — was firing errors every cycle, claiming tasks that were already claimed, spinning on high-priority work it could never actually execute. By the end of the day, we had a live knowledge substrate that every agent and every human on the team reads from the same source.

That arc is worth documenting, because the pieces only make sense as a whole.

Fixing the Brain First

Before anything else could work, the orchestration loop had to work.

cortex-events was hardcoded to gemma-4-31b-it, a model we route through Google’s free tier. When that model hits its rate limit — which it does, regularly — the service crashes and restarts, taking the entire brain loop with it. We added two environment variables: BRAIN_MODEL and BRAIN_CONTENT_MODE. Model changes now happen without code deploys. Content posting toggles without service restarts. Neither of these should have been hardcoded to begin with.

The stale-claim bug was more subtle. The sovereign loop’s logic was: find the highest-priority unclaimed task, claim it, execute it. The problem came when a task had been claimed by a different process. The loop would find that task, attempt a claim, fail, and return early — then on the next cycle, find the same task again, fail again, and loop forever. The fix was a retry window: rather than returning on a failed claim, the loop walks up to ten candidates before giving up. In practice, this eliminated the starvation pattern entirely.

Once the loop was stable, we built /dashboard/brain: an SVG arc gauge for cycle success rate, a KPI row, a sparkline across the last 20 cycles, and a full cycle history table. Brain.py now POSTs telemetry to D1 after every cycle. The dashboard auto-refreshes every 30 seconds. Before this, diagnosing a brain problem meant reading raw logs. Now the signal is visible.

Team Profiles and Access Control

A coordination substrate needs to know who its people are. We built rich profiles — bio, title, skills, squad role (closer, setter, engineer, facilitator, or agent), and project affiliations — and a team directory that renders a role-gated grid of all members. Each card shows avatar initials, skills, and join date.

The RBAC layer behind this is a requireRole() factory in the Worker — the same pattern we’ve written about before, but now applied uniformly across every team-facing surface. Enforcement happens server-side. The client-side UI simply reflects what the server will allow.

The reason this matters: the people using the platform include both human reps and AI agents. A GAF closer and an autonomous agent both have profiles. Both have roles. Access control that works for humans works for agents — and vice versa — because the primitives are the same.

The Knowledge Base

We built kb_articles: a table with slug, title, body, category (onboarding, playbook, reference, or compliance), project scope, and required role. Project scope is the key design decision. Each project — GAF, Substrate, Academy, DNU, TROP, ToRivers — gets its own knowledge pool. A GAF closer sees GAF playbooks. A substrate engineer sees sprint docs. The same UI and the same export endpoint serve all of them, because the schema is identical. Every project is a first-class instance, not a configuration variant.

This matters operationally. When a new team member joins the GAF sales operation, they see one knowledge base scoped to their project and their role. They don’t see the kernel sprint docs. They don’t see another customer’s onboarding materials. The scoping isn’t a permission denial — it’s just the right shape of information for the person looking at it.

The Sync Loop

A knowledge base is only as good as what’s in it. We didn’t want a manual curation step.

kb-sync.py walks a configured set of directories — agents/loom/briefs/, docs/plans/, agents/mizan/contacts/, content/en/blog/ — reads each markdown file, extracts YAML frontmatter, and POSTs to /api/internal/kb/sync. A systemd timer runs this every 30 minutes. Frontmatter keys kb_project, kb_category, kb_role, and kb_skip override the defaults. If a file has no frontmatter, the script classifies by directory.

# Auto-classify by directory if no frontmatter override
DIR_DEFAULTS = {
    "agents/loom/briefs":    {"project": "substrate", "category": "playbook"},
    "docs/plans":            {"project": "substrate", "category": "reference"},
    "agents/mizan/contacts": {"project": "gaf",       "category": "reference"},
    "content/en/blog":       {"project": None,         "category": "reference"},
}

The result: every brief, plan, and contact file written by any agent flows into the knowledge base without manual curation. An agent writes a sprint brief at 2 AM. By 2:30 AM, it’s a knowledge base article, indexed, scoped, and retrievable by any team member or downstream agent with the appropriate role.

This is the mechanism behind the composability. The brain writes a brief. The sync loop picks it up. The KB article exists. An agent reads it via the project-scoped API. A human reads it through the team interface. The knowledge doesn’t wait for anyone to file it.

NotebookLM Export

We added one more consumption path: GET /api/nb/:token/:dept/:chunk. Signed token, department filter, chunked at 48,000 characters — NotebookLM’s document limit — and served as a file attachment.

The reason this endpoint exists is that NotebookLM generates audio summaries from source documents. If you upload your KB to NotebookLM, it produces a podcast episode about the content. That’s a useful format for humans to consume dense operational material — sprint summaries, playbooks, contact notes — without reading. The chunking and department filter exist so you can give NotebookLM exactly the slice it needs without feeding it everything at once.

The important constraint: the KB that feeds agent retrieval is the same KB that generates NotebookLM source files. We didn’t build a separate export pipeline. One source of truth, two consumption paths. Obsidian vault export and Notion two-way sync are queued as next carries.

What “Agents as First-Class Team Members” Actually Requires

Each individual piece built today — the brain dashboard, the team profiles, the knowledge base, the sync loop, the export — is useful on its own. But the reason we built them in a single session is that they’re a system, not a feature list.

The brain writes a brief. The sync loop ingests it into the KB. The KB article becomes retrievable by any agent or human with the right project scope. The human downloads it for NotebookLM. NotebookLM generates a podcast episode about the work. The knowledge doesn’t live in any one person’s head or any one file. It flows.

The deeper point is about what “agents as first-class team members” actually requires at the infrastructure level. An agent needs the same primitives a human needs to function on a team: structured memory, access control, searchability, and export. The difference is that agents need those primitives to be machine-readable first, with human-readable formats as a second derivation — not the other way around.

Most knowledge management tools are built for humans and scraped by agents. We built this for agents and rendered it for humans. That inversion is small in implementation but significant in practice: agents get reliable, structured, scoped access. Humans get a view into what the agents know.

What this enables next is consistent onboarding across projects. The same KB mechanism that tracks sprint briefs for the substrate team will track grant applications for the GAF team. A new agent minted for any project gets immediate access to that project’s full operational history — not because we curated it, but because the sync loop ran while the work happened.


The knowledge base, sync loop, and NotebookLM export are part of the Mumega coordination substrate. The brain dashboard is live at /dashboard/brain for team members with substrate access.

Share