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.
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.
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.
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>' --jsonJobs' 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.
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.
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.
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.
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:
- Preserve shared contract blocks and personal prose. Move personal prose outside generated markers, retaining its complete text.
- Put module conventions in the module's
instructionscontribution or its validated config, then replace mixed sections with owned slots/regions (or remove reviewed obsolete section markers and let enable derive the regions). - 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.
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.
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 anchorstatus.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.mdrecords where it stands in frontmatter:stage(researching→applied→screening→interviewing→offer, orclosed),fit(strong/medium/weak),applied(date),next_step, whose date goes in the coredeadline, andclosed_reason. Closing also setsrelevance: historical.brain jobs pipelinegives the opportunities'_index.mda registry spec (an Active and a Closed table bystage), after whichbrain registryandbrain maintainkeep it current. Two audit checks (jobs-stage, info) flag astatus.mdwithout astageand one stillresearchingafter 60 days. - CLI word:
brain jobs—scrape,score,triage,review,stats,scaffold <id>,pipeline,show,open,decide,search,gc.scrape --browseradds browser boards to the configured selection;--browser-onlyselects only browser boards. Selected adapters receive Chrome automatically. - Cron: advisory
scrapedaily at 06:00 runs plainjobs scrape, following the same effective module settings as a manual run without adding boards. Omitted boards use curated defaults; explicitboards: []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.
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 anchorsstatus.md,itinerary.md,outline.mdremain; slide-deck segmentsalt-decks,versions,deckremain excluded. - Config: an empty block for new users. Deprecated
travelPartyremains 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.
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/; sharedstatus.md,itinerary.md,outline.mdanchors. 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>andbrain travel sync [--check]; all have documented--jsonenvelopes. Sync regeneratestrips/_index.mdandplaces/_index.mdfrom 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.
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 aledger.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 attemplates/ledger.md. - Skills: none bundled.
- CLI word:
brain finance— the AR report;sync(regenerate ledger tables and theclients/_index.mddashboard 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 bybrain audit.
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, defaultassets/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_KEYand/orGEMINI_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,modelsto list what is available.--aspect/--resolutionwork on every model;--ref,--mask,--transparent,--sizeroute by capability;--dry-runprices a decision without spending,--drafttakes 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-runshows the choice without generating. A displayed cost is an estimate where the provider's billing is not yet known.
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.
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 upbrain 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.
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.
- concepts.md — the taxonomy model modules extend.
- configuration.md — the
modulesconfig key. - extending/README.md — modules vs. provider seams.
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.
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.