SPEC — Mímir Module System
Reusable skill rails + last30days as the first module.
1 Problem & Goal
Mímir gains capabilities as 20 flat Mastra tools, each hand-imported and hand-registered in
src/lib/mastra-agent.js, named inconsistently (TOOL-01, LIVE-02,
ICEL-01, AGNT-01, PREF-01). There is no unit of "a skill," no slash-command
surface (zero app.command() listeners today), no end-user guide surface, and no way for Mímir to explain
its own capabilities. Adding a feature touches several files with no contract.
src/modules/<name>/ folder + one line in the registry list.last30days as the first module on those rails. The 20 legacy tools are left untouched and
migrated incrementally later. No legacy refactor in v1.2 Definitions
- Module — a self-contained skill under
src/modules/<id>/, declaring its contributions viamanifest.js. The unit of surgical operation. - Manifest — single source of truth for a module: tool, slash subcommand, App Home contribution, help/self-explain prose, env contract.
- Registry —
src/modules/registry.js. Reads all manifests; exposes the wiring the surfaces consume. Owns built-inhelp/explainand the self-explain tool. - Surface — NL (agent tool selection), Slash (
/mimir <sub>), App Home (Skills directory + guide). - Logic core — a module's pure implementation under
logic/. Both adapters call it; it knows nothing about Slack or Mastra.
3 Module folder layout
src/modules/last30days/
manifest.js # THE CONTRACT — SSOT (tool, command, home, help, env)
tool.js # Mastra tool — thin adapter (NL invocation)
command.js # /mimir subcommand handler — thin adapter (slash)
home.js # App Home guide card + launcher (optional)
logic/ # pure core — no Slack/Mastra imports
reddit.js hn.js polymarket.js github.js brave.js
score.js synthesize.js
last30days.test.js # co-located node:test
README.md # human doc, conformance-checked against manifest
tests/) for surgical isolation;
(2) explicit registration array (not glob auto-discovery) for determinism + obvious diffs.
"Add your module to MODULES" is the documented rail step.4 The manifest (contract)
src/modules/<id>/manifest.js default-exports this exact shape:
export default {
// Identity
id: 'last30days', title: 'Last 30 Days', version: '1.0.0',
status: 'active', // 'active' | 'beta' | 'disabled'
// NL invocation (agent tool selection)
tool: last30daysTool, // a Mastra createTool() instance, or null
// Slash invocation (/mimir <sub>)
command: {
name: 'last30days', // subcommand token; equals id for flagship module
usage: '/mimir last30days <topic> [--finance]',
handler: runLast30daysCommand, // (args, ctx) => Promise<void> ; or null
},
// App Home (Skills directory)
home: {
card: buildLast30daysCard, // () => Block[] for the Skills row; or null
guide: buildLast30daysGuide, // () => Block[] for guide modal; null → help.long
},
// Self-explain SSOT (App Home + /mimir help + explain tool all read this)
help: {
short: 'Multi-platform recency research — what communities said in 30 days.',
long: '<full how-it-works prose>',
examples: [ '/mimir last30days uranium --finance',
'what are people saying about NVDA lately' ],
},
// Operational contract
env: { required: [], optional: ['BRAVE_API_KEY'] },
};
help is the only place a module's explanatory prose lives. App Home guide,
/mimir help, and the self-explain tool all read it. No duplicated docs. Any of tool /
command.handler / home.card may be null; the conformance test requires at least one of
tool or command.handler non-null.5 The registry
import last30days from './last30days/manifest.js';
export const MODULES = [ last30days ]; // add new modules here
getModuleTools() // → { [toolId]: tool } for active modules,
// PLUS the registry's built-in explainSkillTool (§7)
routeCommand(sub, args, ctx) // dispatch /mimir <sub>; handles built-ins help + explain
getHomeCards() // → Block[] aggregating each active module's home.card
getModuleHelp(id) // → module.help (or not-found shape)
listModules() // → [{ id, title, status, help.short }]
Built-in subcommands (registry-owned): /mimir help lists active modules ·
/mimir explain <id> returns help.long + examples · /mimir <id> <args> routes to the
module · unknown → "try /mimir help".
try/catch; a
module that throws or is status !== 'active' is skipped (logged). One bad module can never break agent boot.6 Wiring into the three surfaces
6.1 NL — agent tool selection
mastra-agent.js spreads registry tools beside the 20 legacy tools. The tool description is the NL trigger the LLM matches on.
import { getModuleTools } from '../modules/registry.js';
tools: { searchByOrgTool, /* …20 legacy… */ ...getModuleTools() }
6.2 Slash — /mimir <sub>
index.js registers one Bolt command. The handler ack()s immediately (3-second rule),
splits the text, and calls routeCommand; work runs async and posts via chat.postMessage.
/mimir slash command in the Slack app manifest.
After this, no per-module Slack config is ever required — new modules add subcommands purely in code.6.3 App Home — Skills directory + guide modal
app-home.js gains a Skills section from getHomeCards(). Each card shows
help.short + a Guide button (home_skill_guide_<id>) opening a modal built from
home.guide() (or help.long + examples if null).
7 Self-explain — Mímir teaches the skill
A registry-owned meta-tool explainSkillTool — emitted by getModuleTools() alongside module tools —
lets Mímir answer "how does last30days work?", "what skills do you have?", "what can you do?". It reads manifests and
speaks the same help SSOT as App Home and /mimir help. Three surfaces, one truth.
8 Data flow (both paths share one logic core)
tool.js and command.js are thin adapters — parse + post only. All real work lives in
logic/, shared. Change behavior once, both surfaces follow.
--finance forces it on; absence + non-watchlist topic → general framing.9 Error handling
- Per-source:
Promise.allSettled— a failed/empty source is omitted + noted (mirrorsresearch-orchestrator.js). One dead platform never fails the call. - Missing optional env: source skipped with a logged note; module runs on remaining free sources.
- Module isolation: registry try/catch → broken module degrades to "skill unavailable", never a boot failure.
- Slash 3-second rule:
ack()before any work; a "researching…" placeholder posts immediately (reuse thethinkPostpattern fromweb_search), the brief posts when ready.
10 Testing & rails enforcement
registry.contract.test.js— the conformance rail. Per module: unique kebab-caseid= folder name; validcommand.name; ≥1 oftool/command.handlernon-null;help.short+help.longpresent;envshape;statusin allowed set. Fails CI if any module drifts.- Per-logic-file unit tests (
node:test) with dependency-injected fetchers (same injectable pattern asresearch-orchestrator.runResearch) — no network. - Registry routing test —
routeCommanddispatch + built-ins behave correctly.
11 Documentation
docs/specs/SPEC-mimir-module-system.md— this design.docs/architecture/MODULES.md— the canonical rail doc: manifest reference + copy-paste "How to add a module" checklist.- Per-module
README.md— conformance-checked against its manifest. docs/TOOLS.mdupdated to point module tools at their owning module.
12 Build order
| Step | Deliverable | Behavior change |
|---|---|---|
| M1 | Manifest contract + registry.js + registry.contract.test.js | none (rails only) |
| M2 | Wire registry into 3 surfaces (agent spread · /mimir command + built-in help/explain · App Home Skills + guide modal) + a stub module proving wiring | /mimir help works |
| M3 | last30days logic core (5 fetchers + score + synthesize) + injected tests | none (not wired) |
| M4 | last30days adapters + manifest.js + finance preset + add to MODULES | skill live on 3 surfaces |
| M5 | Docs + operator step: declare /mimir in Slack manifest | guide + self-explain live |
The system (M1–M2) is the reusable rails; last30days (M3–M4) is the first module proving them.
13 Out of scope (v1)
Legacy migration
The 20 existing tools stay flat; migrate incrementally later.
Gated platforms
X · YouTube · Bluesky · TikTok/IG/Threads (ScrapeCreators). v2, once headless-cred/runtime solved.
HTML emit
--emit=html / file output from the upstream skill.
Native aliases
Per-skill Slash aliases — umbrella /mimir <sub> only in v1.
v1 source set
Free headless-safe only: Reddit · Hacker News · Polymarket · GitHub · Brave.
14 Open items for review
- Confirm
src/modules/as the location (vssrc/lib/modules/). - M2 stub module: keep as a living
src/modules/_example/template (conformance-covered, referenced by the checklist) vs throwaway. Recommendation: keep it.
Mímir Agent · Viska Capital · spec rendered for review · commit 4840bf2 (local, needs viska-pm relay)