SPEC — Mímir Module System

Reusable skill rails + last30days as the first module.

Status  Design approved 2026-06-17 Author  Mímir Agent Class  D — spec doc Commit  4840bf2 (local)

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.

Goal — a reusable, documented, rails-enforced path to add a skill as a self-contained module. One module folder declares everything via one manifest; a central registry wires it into three invocation surfaces (agent-tools / slash / App Home) plus a docs-and-self-explain plane. Adding a future skill = a new src/modules/<name>/ folder + one line in the registry list.
v1 scope (approved) — define the full module system, then implement 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

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
Rail choices (approved) — (1) tests co-located inside the module folder (not repo 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'] },
};
SSOT rule — 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".

Boot safety — discovery and every accessor wrap per-module access in 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.

Operator step (one-time) — declare the /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)

@Mimir "buzz on uranium" mention-mastra agent picks tool tool.js
/mimir last30days uranium mimir-command.js routeCommand command.js
↓ both → logic core: allSettled([reddit,hn,polymarket,github,brave]) score synthesize (finance preset) { slackText }

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 preset — synthesis checks whether the topic matches a Viska watchlist entity; if so it frames a research-analyst note (sentiment, catalysts, contrarian views). --finance forces it on; absence + non-watchlist topic → general framing.

9 Error handling

10 Testing & rails enforcement

11 Documentation

12 Build order

StepDeliverableBehavior change
M1Manifest contract + registry.js + registry.contract.test.jsnone (rails only)
M2Wire 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
M3last30days logic core (5 fetchers + score + synthesize) + injected testsnone (not wired)
M4last30days adapters + manifest.js + finance preset + add to MODULESskill live on 3 surfaces
M5Docs + operator step: declare /mimir in Slack manifestguide + 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


Mímir Agent · Viska Capital · spec rendered for review · commit 4840bf2 (local, needs viska-pm relay)