Skip to content

Latest commit

 

History

History
681 lines (571 loc) · 36.9 KB

modules.md

File metadata and controls

681 lines (571 loc) · 36.9 KB

Modules

A module packages a domain workflow — its document types, its skills, and optionally a CLI command — so you can turn a whole area of your life on with one config entry. The first-party modules are listed below; /new-module helps you author your own.

Module configuration, JSON settings and canonical content follow the shared supported-input policy. Each module's linked reference owns its domain field meanings, validation, defaults and migrations. The first-party inventory identifies all five domains and distinguishes canonical files from internal storage.

What a module is

A module is an npm package (or a local directory referenced by path) whose entry default-exports a manifest via defineModule(). There is no plugin daemon and no lifecycle beyond load-time registration.

A manifest is two-phase: static identity (name, configSchema), then a setup(config) function that builds the module's contribution from the user's already-validated config block. This is what lets a taxonomy dir follow a configured directory instead of being a static literal. This manifest excerpt assumes a defined configSchema and a ./cli command implementation; see a first-party module's source for a complete package.

import { defineModule } from "@schlessera/brain";

export default defineModule({
  name: "jobs",
  configSchema,
  setup: (config) => ({
    taxonomy: { types: { opportunity: { dir: config.opportunitiesDir } } },
    commands: { jobs: () => import("./cli") },
    indexRules: { dirAnchors: ["status.md"] },
    cron: [{ name: "scrape", schedule: "0 6 * * *", command: "jobs scrape" }],
  }),
});

Manifest fields:

Field Type Purpose
name string The module's short name. Required.
configSchema zod schema Validates the brain-config block merged with settings/<name>.json. Load fails hard on a rejected block.
settings ModuleSettings<C> Optional data-only editor description, module actions, notes and explicit migration planner.
setup (config) => Contribution Builds the contribution from the validated config. Required.
canBeDormant boolean? Defaults to permitted; false makes CLI disable refuse.
dormancyReason string? Nonempty explanation for that refusal.

Contribution fields (all optional; the returned object is schema-validated at load, and unknown keys are load errors):

Field Type Contributes
taxonomy { types?, classifierHints?, assetTitleRules?, propagation? } Types, capture hints, and rules merged into the effective taxonomy.
skills string Path to the module's skills/ directory (relative to the package root).
instructions { text: string } Optional nonempty authoritative instruction text, generated by setup(config) from validated config.
commands Record<word, () => import(...)> One namespaced top-level CLI word, lazily imported (e.g. brain jobs …).
renderers Record<string, { fences?, extensions?, client, static? }> Content renderer export declarations; see content renderers.
tools Record<localName, () => import(...)> Lazy MCP tools served as <module>_<localName>; each wraps the same operation as its CLI subcommand.
hygieneChecks ((ctx) => AuditIssue[] | Promise<AuditIssue[]>)[] Extra checks surfaced by brain audit and counted in brain maintain's audit step; a check that throws becomes one module-hygiene warning. ctx is { queries, root, config }, with config typed by your configSchema. See hygiene checks.
indexRules { dirAnchors?: string[] } Directory anchor files (for [[dir/]] wiki-link resolution).
exclude { segments?: string[] } Path segments the indexer should skip.
cron { name, schedule, command }[] Advisory schedules consumed by container entrypoints and brain doctor.

Module commands receive { root, json, config, taxonomy } — the module's own validated config block and the fully-merged taxonomy — so they never re-load brain.config themselves.

defineModule threads the configSchema's parsed type end to end: setup(config), ctx.config in every command's run(), and ctx.config in every hygiene check are all the schema's output type. No cast, no re-parse — the loader already failed hard on an invalid block, so what arrives is the validated value with defaults applied. A command that lives in its own file names the type once:

import type { CommandContext, CommandModule } from "@schlessera/brain";
import type { JobsConfig } from "./module.js";

const command: CommandModule<JobsConfig> = {
  summary: "…",
  async run(args, ctx) {
    ctx.config.boards; // typed string[] — straight off the schema
    return 0;
  },
};
export default command;

Modules that ignore config (or declare no configSchema) need none of this — the parameter defaults to unknown and everything compiles as before. bun run lint refuses ctx.config as X in first-party module sources (scripts/check-module-casts.ts): the generic makes the cast unnecessary, and a cast would hide the contract regressing back to unknown.

How the contribution merges into the taxonomy — collisions, ordering, overrides — is covered in concepts.md.

Hygiene checks read the index through ctx.queries

A hygiene check receives { queries, root, config }. queries is a frozen object core builds for each audit, bound to this brain's root. Its nine methods are the supported content-index queries (readGraphMeta, readGraphClusters, readGraphNeighborhood, readGraphDiscovery, readGraphMaintenance, readLinkWalk, readVoiceVocabulary, listIndexDocuments, findIndexDocuments). Each takes that function's options without brainPath and returns the same QueryResult. A brainPath passed anyway is ignored, and so is a later change to ctx.root: core captured the root once. Each call reads its own short-lived snapshot. No database handle, SQL or connection is exposed.

import type { AuditIssue, HygieneContext } from "@schlessera/brain";
import type { VoyageConfig } from "./module.js";

export function checkVoyageLogs(ctx: HygieneContext<VoyageConfig>): AuditIssue[] {
  const found = ctx.queries.findIndexDocuments({ type: "voyage-log", excludeStatus: "archived" });
  // A failed read must fail the check; an empty list is a valid clean index.
  if (!found.ok) throw new Error(`voyage logs could not read the index (${found.error.code})`);
  return found.value
    .filter((doc) => doc.path.startsWith(`${ctx.config.logsDir}/`))
    .map((doc) => ({ path: doc.path, severity: "info", category: "voyage", message: "Log Ithaca's arrival" }));
}

Throw on ok: false, as above: the audit reports the thrown check as one module-hygiene warning and brain hygiene reconcile keeps that module's entries open, whereas returning [] would report a clean result for an index it never read. Read source frontmatter yourself under ctx.root; the index supplies the candidate paths. The option, result and error types (QueryResult, QueryCode, DocumentCandidate, …) are exported from @schlessera/brain/queries; ContentIndexQueries is exported from both entries.

Migrating from ctx.db (breaking in 0.41.0). Before it, the context carried a raw db connection to brain.db. Replace each SQL read with the query that returns the same rows, and keep any path or case matching your old WHERE clause did in module code. findIndexDocuments returns the complete path-ordered set without a cap; its pathSuffix is a literal, case-sensitive suffix, not a LIKE pattern. The jobs module's opportunity check, for example, filters that complete set for the former path = 'status.md' OR path LIKE '%/status.md' (isStatusFile, packages/module-jobs/src/pipeline.ts:200-203). A module with its own store (such as the jobs module's jobs.db) keeps opening that itself.

Editable module settings

Settings → Modules lists configured workflows, including dormant modules and unavailable entries with their error. Its forms use the declaration's Zod schema and optional settings labels, help, choices and nested descriptions. New schema fields appear without a UI change; unsupported JSON values remain read-only and are preserved on save. A module supplies data and CLI actions, never React settings components. Content renderers are the narrowly scoped exception below.

Values are stored in settings/<name>.json. Objects merge per key over the brain-config domain block; arrays and scalars replace. The same schema checks load, brain validate, CLI saves and UI saves. Defaults remain in the schema, and untouched optional inputs stay absent. TypeScript retains runners and other logic. Inherited values require an explicit Override; Reset removes a saved override. Save/Discard protects edits across closing, section changes and responsive resizing. A stale revision requires choosing between the local and saved values before resubmission. Actions and dormancy changes require a separate confirmation and use saved settings.

brain module settings jobs --json
brain module settings jobs --set scoring.queueThreshold=65 --json
brain module settings jobs --set scoring.groups.0.tiers.0.points=20 --json
brain module settings jobs --migrate --preview --json
brain module settings jobs --migrate --revision '<preview revision>' --json

Jobs' explicit migration previews its complete legacy scoring source, proves parser equivalence, and moves known scoring keys into JSON in one commit. Unrelated frontmatter, unknown scoring keys and prose stay in the criteria file. No migration happens merely by opening Settings. The source format and transaction envelopes are specified in the integration contract.

Authoring MCP tools

Available in 0.40.0+. Published 0.39.0 serves only the core MCP tools.

Declare a tool when an existing CLI operation needs to be reachable from an MCP client without a shell. Keep its deterministic operation in one function that both the CLI subcommand and tool call. Declare each tool explicitly; CLI commands are not exported automatically.

// Inside defineModule({ name: "catalog", configSchema, setup: … })
setup: () => ({
  tools: { lookup: () => import("./mcp/lookup.js") },
}),

Core composes catalog_lookup. A module with tools uses a lowercase name matching ^[a-z][a-z0-9-]{0,30}$, and cannot be named brain. Local names match ^[a-z][a-z0-9_]{0,31}$. The full name is at most 64 characters and cannot replace or shadow a core tool. Modules without tools retain their existing naming rules.

Author the definition with defineModuleTool:

import { defineModuleTool } from "@schlessera/brain";
import type { ToolContext } from "@schlessera/brain";
import { z } from "zod";
import type { CatalogConfig } from "../module.js";
import { lookupCatalog } from "../operations.js";

export default defineModuleTool({
  description: "Look up catalog entries. limit defaults to 10 and permits at most 50 entries.",
  inputSchema: z.strictObject({
    query: z.string().describe("Text to find in the catalog"),
    limit: z.number().int().min(1).max(50).default(10).describe("Maximum entries to return"),
  }),
  outputSchema: z.strictObject({
    entries: z.array(z.strictObject({ title: z.string() })),
  }),
  annotations: { readOnlyHint: true, openWorldHint: false },
  async run(input, ctx: ToolContext<CatalogConfig>) {
    return lookupCatalog(input, ctx);
  },
});

The helper infers input and the result from the schemas. ToolContext<C> provides root, the owning module's validated config, the effective taxonomy and the request's abort signal. Use that config directly, with no cast or re-read. Both schemas must be strict zod 4 objects representable as JSON Schema; every input needs a description. State defaults and result caps in the tool's description, and enforce the cap in the shared operation.

readOnlyHint and openWorldHint are required. A writing tool also states destructiveHint; idempotentHint and a title are optional. These are client hints, not permission grants. Backend permissions are decided by tool name.

The module README must have a level-two MCP tools section naming every canonical tool, with inputs, defaults, limits, result shape and compatibility ownership. For example:

## MCP tools

`catalog_lookup` accepts query and limit (default 10, maximum 50) and returns
{ entries: [{ title }] }. This read-only tool calls the same catalog lookup
operation as the CLI. The module owns this supported name and schema under
its documented versioning policy.

Run brain module lint catalog --json before shipping. Tool findings use tool-load for import/general definition failures, tool-name for naming errors, tool-annotations for required hints, tool-schema for invalid schemas or undescribed inputs, and tool-docs for the README section. Annotation and schema failures use their specific rule, so one defect does not also produce a duplicate tool-load finding. A rejected name remains a load error for normal commands and receives a tool-name diagnostic in lint.

brain module list --json includes each enabled module's declared canonical tools in declaration order, or []. Listing does not import definitions. MCP startup uses the same definition validator as lint; one invalid definition prevents every tool from that module from registering. Lint also checks input descriptions and README coverage. See mcp.md for discovery and the fixed process lifecycle.

Each module owns the names, schemas and behavior of its supported tools. First-party tools enter the integration contract and follow the project's versioning rules; third-party modules document the same policy in their own packages. Namespacing prevents collisions and does not exempt a tool from compatibility. The authoring exports are experimental until 1.0. See the module-tool decision.

Enabling and disabling modules

Every entry under modules: remains configured and loaded. A key is a package name or a local path; the value keeps the module's domain configuration. The optional core-owned enabled boolean defaults to true. Core validates and removes that flag before handing the remaining block to the module's schema, including when enabled: false makes the module dormant.

Install a package before enabling it, for example bun add @schlessera/brain-module-speaking. Keep first-party module versions aligned with your installed @schlessera/brain release. A local module must exist at the configured path. After a manual config edit, run brain config check and brain skills sync.

modules: {
  "@schlessera/brain-module-speaking": {},
  "@schlessera/brain-module-finance":  { clientsDir: "clients", feeTolerance: 30 },
}

Use brain module disable <name> to park a configured workflow and brain module enable <name> to reactivate it. Both synchronize managed skill links and regenerate explicitly owned instruction regions; the second unchanged run writes nothing. Domain config and documents remain in place. Literal JSON and direct default-exported TypeScript/defineConfig objects are edited at the flag only, preserving surrounding code and comments. Ambiguous dynamic targets are refused; review and edit the flag explicitly before retrying the command.

A dormant module keeps its types, directory anchors, exclusions and content rules, so existing documents validate, index, search and link normally. Its managed skills disappear from .agents/skills/ and emitter outputs, its owned instructions disappear, its classifier hints and module audit/hygiene checks are skipped, and scheduler-facing module list metadata has no cron entries. Previously installed cron commands reach the dormant CLI namespace guard, which exits 1 and names the enable command. maintain does not run module cron jobs; it skips dormant checks in its audit step. User-owned skill directories and shared installed-contract blocks are preserved.

brain module list --json retains { enabled, available }; enabled is the historical field for all configured modules, including dormant ones. Entries add state: "active" | "dormant" and contextTokens, a nonnegative integer estimate of active context using the existing characters/4 estimator. It counts that module's discoverable model-invocable skill descriptions under normal discovery precedence, plus its authoritative instruction text once. It does not charge shared contracts or personal prose, multiply across agent entry files, or count full skill bodies. The estimate remains available while dormant. Declared tools remain listed, so the owner can see what activation would add. At MCP startup, tools from dormant modules are neither imported nor registered. Running sessions keep their loaded state and registered tools; the next process picks up a dormancy edit. Dormancy controls context and does not revoke permission or cancel in-flight operations.

Instruction contributions

Module authors may return instructions: { text } from setup(config). The object is strictly validated, text must be nonempty, and ownership markers cannot appear in contributed text. Instruction owners use the same lowercase name shape as modules with tools. Existing manifests need no instruction field. First-party modules supply their own conventions through this field; there is no backup of generated prose or authoritative copy in brain.db.

Each contribution uses one module-<name> region after a persistent ownership slot. The slot preserves position when the generated region is absent:

<!-- brain:module-instructions:jobs -->
<!-- brain:generated:module-jobs -->

…the jobs contribution's text…

<!-- /brain:generated:module-jobs -->

The slot and closing marker each end with a newline; the region immediately follows its slot. Disable removes the region, leaving its slot. Enable derives the text again from validated configuration at the same position. Other regions and every byte outside replaced owned spans remain unchanged. The CLI maintains existing CLAUDE.md, AGENTS.md and GEMINI.md files and creates entry files needed by configured built-in emitters. When CLAUDE.md imports AGENTS.md, module text stays in AGENTS.md instead of being loaded twice. Shared contracts remain independently managed by the emitters. Unknown owners, duplicate/malformed markers, nested regions and path escapes are refused before toggling.

Instruction migration

Legacy generated navigation/conventions/structure/personal-rules sections do not identify paragraph owners. Both toggles refuse them before changing config, managed skill links or instruction bytes, including an otherwise idempotent toggle. Explicit migration is a user/agent edit, reviewed before retrying:

  1. Preserve shared contract blocks and personal prose. Move personal prose outside generated markers, retaining its complete text.
  2. Put module conventions in the module's instructions contribution or its validated config, then replace mixed sections with owned slots/regions (or remove reviewed obsolete section markers and let enable derive the regions).
  3. Review that both modules' text and all personal prose survived. Run the enable/disable command, then validate and inspect the diff.

The CLI never infers paragraph ownership from module names. There is no limited-success toggle that leaves dormant generated prose in context. The binding ownership and explicit-migration rulings explain why. /brain-module handles the interview, initial directories and review; CLI commands execute the deterministic state/context change. Use those commands after manually editing an enabled flag so owned instructions are also regenerated; skills sync alone only refreshes skill discovery and emitter output.

First-party modules

The opt-in @schlessera/brain-module-diagrams contributes brain diagram check <file|-> --json and upstream review tooling. The v1 spec contract defines standalone and fenced YAML, strict family grammars, finding envelopes, budgets and free-layout geometry checks. Its client/static packs render the shipped node-link, ordered-axis and positional families through the configured host. The diagram authoring skill teaches selection, taste and fence/check workflow, loading type references and worked examples on demand. brain module enable diagrams synchronizes that skill and its generated module instructions. Use Mermaid for a quick sketch; pictorial illustrations and image edits remain image-gen work.

This list follows main. Travel and module-contributed MCP tools join the next release (0.40.0); they are absent from published 0.39.0. Speaking's travel ownership changes with that release, so use the linked migration guide when upgrading rather than copying a new config into an older install.

@schlessera/brain-module-jobs

Full reference: jobs package.

A personal job-search pipeline: it scrapes remote-job boards, deduplicates and full-text-indexes postings in its own SQLite database (jobs.db), scores each against criteria you define, and gives you a CLI to triage and scaffold opportunity notes.

  • Types contributed: opportunity → career/opportunities/ (dir anchor status.md).
  • Config: criteria (required — path to a scoring-criteria markdown file whose frontmatter supplies legacy scoring until explicitly migrated to JSON), opportunitiesDir, boards, queries, dbPath. Scoring is not hard-coded — it is driven by the criteria file, which stays brain content you own and tune.
  • Skills: jobs-review (triage the scraped queue in dialogue), research-opportunity (turn a listing into a tracked opportunity with a fit assessment), interview-scheduled (record a booked interview and sync the four places that have to agree about it).
  • Pipeline fields: an opportunity's status.md records where it stands in frontmatter: stage (researching → applied → screening → interviewing → offer, or closed), fit (strong/medium/weak), applied (date), next_step, whose date goes in the core deadline, and closed_reason. Closing also sets relevance: historical. brain jobs pipeline gives the opportunities' _index.md a registry spec (an Active and a Closed table by stage), after which brain registry and brain maintain keep it current. Two audit checks (jobs-stage, info) flag a status.md without a stage and one still researching after 60 days.
  • CLI word: brain jobs — scrape, score, triage, review, stats, scaffold <id>, pipeline, show, open, decide, search, gc. scrape --browser adds browser boards to the configured selection; --browser-only selects only browser boards. Selected adapters receive Chrome automatically.
  • Cron: advisory scrape daily at 06:00 runs plain jobs scrape, following the same effective module settings as a manual run without adding boards. Omitted boards use curated defaults; explicit boards: [] selects nothing and succeeds without scrape requests. Unknown or retired names reject the whole selection through the shared load/save validator. See the selection decision.
  • Caveat: scraping may violate a board's Terms of Service — review each board's ToS and robots.txt, keep volume low, and prefer official feeds. See the module README.

@schlessera/brain-module-speaking

Full reference: speaking package.

The speaking lifecycle: researching conferences, developing talk ideas, submitting to CFPs, tracking outcomes, preparing talks and wrapping up afterward.

  • Types contributed: talk → talks/, conference → conferences/. Shared anchors status.md, itinerary.md, outline.md remain; slide-deck segments alt-decks, versions, deck remain excluded.
  • Config: an empty block for new users. Deprecated travelParty remains accepted during the travel transition, with an actionable warning when nonempty.
  • Skills: conference-research, talk-ideas, brainstorm-talks, new-submission, submission-outcome, talk-prep, conference-aftermath. Speaking links to travel's planning capability and existing journey documents.
  • CLI word: none.

@schlessera/brain-module-travel

Standalone journeys, repeated day trips and visited places. The package joins the next lockstep release. See its README for canonical formats and the complete upgrade path.

  • Types contributed: travel → travel/, trip → trips/, place → places/; shared status.md, itinerary.md, outline.md anchors. Travel alone brings no conference types or slide-deck exclusions.
  • Config: travelParty, an array of {name, role?, requirementsDoc?}; default []. Existing paths and values move losslessly from speaking.
  • Skills: plan-travel, for conference-linked and personal journeys; trip-log, for proposing, recording and dismissing day trips with their routes, photos and choosing rules; places, for visited places.
  • CLI word: brain travel validate, brain travel migrate [--dry-run], brain travel photo <files> --to <dir>, brain travel route <url|file> --to <dir> and brain travel sync [--check]; all have documented --json envelopes. Sync regenerates trips/_index.md and places/_index.md from canonical records, preserving prose outside their generated regions. Route import supports local/direct GPX and public Komoot pages, with optional distance trimming. Outdooractive routes are imported from a GPX the user exports while signed in.
  • Migration: install/enable travel; preview/apply the source-only command when neither speaking nor travel has saved settings, otherwise use the README's reviewed manual upgrade. Travel has no shared settings migration planner. Restart, validate and sync skills afterward. Documents keep their paths, types, links and bytes; conflicts and ambiguous source targets refuse without writes. Saved JSON overrides the config block, and party arrays replace in full rather than merging members.
  • History: visit IDs belong to their canonical journey/trip document; place references deduplicate that pair. Unknown dates/coordinates remain unknown, and counts/date bounds derive from visits rather than stored totals.

@schlessera/brain-module-finance

Full reference: finance package.

Accounts-receivable tracking for freelance/agency work. Per-client ledger.md files are the source of truth; the module derives every balance, status, aging bucket, and reconciliation warning from their frontmatter.

  • Types contributed: finance → clients/ (one sub-directory per client, each with a ledger.md).
  • Config: clientsDir, feeTolerance (max per-invoice shortfall still treated as paid), currency, termsDays. The ledger frontmatter spec (invoices, payments, allocations, derived statuses) is documented in the module README, with an annotated starter at templates/ledger.md.
  • Skills: none bundled.
  • CLI word: brain finance — the AR report; sync (regenerate ledger tables and the clients/_index.md dashboard from frontmatter); new-client <slug> (scaffold a ledger from the template).
  • Hygiene check: ledger generated block out of date — flags ledgers whose generated table lags behind their frontmatter, surfaced by brain audit.

@schlessera/brain-module-images

Full reference: images package.

Image generation and editing, routed between OpenAI and Google image models by capability rather than by a configured favourite.

  • Types contributed: none. Generated images are assets that belong wherever the thing they illustrate lives; the module owns no directory.
  • Config: imagesDir (fallback output directory, default assets/images), preferredModels (tie-break order overriding the shipped routing policy), disabledModels (hide a model even when its provider has a key — useful to keep an expensive tier out of reach, or to drop one the account cannot use).
  • Environment: OPENAI_API_KEY and/or GEMINI_API_KEY. Neither is required for listing the catalog; each admits its provider's candidates. A generation call still needs a usable key and the account's model access and quota. Some requests can only be served by one side — see the module README for the routing table.
  • Skills: image-gen (pick the model, price the call, write the file).
  • CLI word: brain image — "<prompt>" to generate, models to list what is available. --aspect/--resolution work on every model; --ref, --mask, --transparent, --size route by capability; --dry-run prices a decision without spending, --draft takes the cheapest model that fits.
  • Caveat: generation uses provider billing. Capability filters run first; where several models fit, configured preferences and the documented shipped policy choose the model. --dry-run shows the choice without generating. A displayed cost is an estimate where the provider's billing is not yet known.

Local (path) modules

A module does not have to be published. Reference a directory in your own repo by path and it loads the same way:

modules: {
  "./modules/catalog": { owners: ["your-org"] },
}

The path resolves relative to the brain root, and brain-kit imports ./modules/catalog/module.ts. This is the mechanism for personal, one-off workflows that never need to be shared — they use exactly the same manifest API as the first-party modules.

Authoring a module

Run /new-module in your agent. It interviews the domain (which types and directories it owns, which lifecycle moments deserve skills, whether a CLI word is warranted), collision-checks the effective taxonomy before scaffolding, then generates the package:

modules/<name>/
  module.ts              # defineModule() manifest
  skills/<skill>/SKILL.md
  README.md
  tests/module.test.ts

and adds the brain.config.ts entry. Before declaring done it runs the quality gates, chiefly:

brain module lint <name>   # validates the manifest, skill frontmatter, and config block,
                           # detects command/type/dir collisions, and checks MCP tools
brain validate             # must be green
brain skills sync          # must pick the new skills up

brain module lint zod-validates the manifest, checks skill frontmatter, runs the skill-lint rules, detects command/type/directory collisions, and verifies the module's configSchema parses your config block. Write the module README to the same structure the first-party modules use — they are the reference examples. If it declares tools, lint also validates their imports, definitions, names, annotations, schemas, input descriptions and README coverage.

The module-authoring entry

A module imports core from two supported entries: @schlessera/brain for the manifest API (defineModule, CommandModule, CommandContext …) and @schlessera/brain/module for the helpers that touch the brain's files the way core does. Both are @experimental until 1.0. Never import @schlessera/brain/internal: it has no compatibility promise, and the first-party modules do not use it either.

Need @schlessera/brain/module exports
Keep a path inside the brain safeResolve, resolveWritable
Write a document atomically writeFileSafely (replace: false refuses an existing name with WriteRefusedError, code: "EEXIST")
Intermediate files that are never canonical SCRATCH_DIR, scratchName, writeScratchFile, assertScratchWritable, isInScratch, isWriteRefusal, pruneScratch
Rewrite a generated region, keeping the rest of the file splitFrontmatterBlock, rewriteGeneratedRegion, inertGeneratedText
Walk the corpus or regenerate registry indexes buildTaxonomy, getMarkdownFiles, runRegistry, ValidationIssue
Call the configured completion provider resolveCompletionProvider(ctx.completions), geminiCompletions, GEMINI_FLASH_MODEL

A command gets the merged taxonomy as ctx.taxonomy and the brain's completions block as ctx.completions. Index reads go through ctx.queries in hygiene checks, or @schlessera/brain/queries.

The approved module instruction ownership and migration policy binds future instruction contributions and onboarding. It requires text derived from validated module config, explicitly owned generated regions, and migration of legacy mixed sections before any toggle side effect. The implementation belongs to #527; its optional contribution API and migration format are documented above.

See also

Video (@schlessera/brain-module-video)

Opt-in brain video watch and /watch analyze a public YouTube URL or local video with Gemini. Configure engine: "gemini", optional model (core's Gemini default) and timeoutMs (300000). It contributes no taxonomy or MCP tool. Watching sends the source and question to Google; the command discloses this before calling and the skill obtains agreement. See the video module README for setup, stable JSON, clip times, unavailable metadata and upload cleanup.

Content renderers

A module may declare renderers from setup(). Each named declaration has optional fences: string[] and extensions: string[], a required client export specifier and an optional static export specifier:

renderers: {
  figure: {
    fences: ["diagram"],
    extensions: [".diagram.yaml"],
    client: "@odyssey/figures/ui",
    static: "@odyssey/figures/render",
  },
}

Both entries must name export subpaths of the declaring package (its package.json name), including for local modules. Unknown declaration fields are errors. Fence names match ^[a-z][a-z0-9-]*$; extensions start with a dot. Configured active modules cannot claim the same fence or extension; the error names both owners. Dormant modules retain their declarations in module listing but make no active claims. mermaid, mmd, .mermaid and .mmd are reserved for the built-in renderer.

brain module lint <name> resolves both entries and verifies that the static entry exports a toStatic function. Ordinary module loading does not import renderer entries. Client entries are bundled by the host build, never imported from user configuration in the browser. The seam and theme contract are described in the renderer decision.

brain module list --json includes each configured module's version (or null) and declared renderers (or {}), also exposed through /api/modules. Core's internal collectModuleRenderers(modules) returns the merged active declarations with module name, renderer name and version for render consumers.

Bundled client packs registered through the SDK content registry render matching fences in chat and Markdown files, and matching standalone text-file extensions in Preview. Exact multi-part extensions take precedence over their shorter suffixes. Raw remains source. Components receive the surface's source, path, mode and resolved theme; share mode uses print. A failed renderer falls back only its own block to source with an accessible status note. While a lazy component loads, source remains visible. Hosts can register React.lazy components backed by a build-time import(); that chunk is requested only when a matching block is rendered. The generic browser bundling integration and server declaration wiring are separate from this dispatch layer.