Skip to main content

Module SDK

A module is a folder and a manifest. This page is everything a person or an agent needs to make one: the fields, what they do to a workspace and to the agent, the lifecycle, the tests, and how it ships. Kept in step with the code by a test.

The reference for making a Sundial module: what one is, the folder and manifest that define it, what every field does to a workspace and to the agent, how a module is created from, connected to, listed and removed, and how to test and ship one. Read it before adding anything under templates/. It is rendered publicly at /docs/sdk (raw markdown at /docs/sdk.md) and is the single source of truth for this vocabulary: when the code changes, this page changes in the same PR (tests/ui/module-sdk-doc.test.ts fails when the registry and this page drift).

Written for two readers: a person adding a module by hand, and an agent asked to "add a module for X". If you are the agent, go straight to §2 and §13.

One word per concept:

TermMeansIn code
ModuleA folder in the repo, templates/<category>/<slug>/, holding a manifest and the files it seeds. Also called a template or an assistant: same thing, older names.TemplateManifest in lib/templates/registry.ts
Manifestmanifest.yaml: name, category, description, the instructions the agent gets, and the optional extras (first file, intro, setup, buttons, checks).parseTemplateManifest
FilesEverything under files/, seeded verbatim into the workspace. Nested paths kept.resolveTemplateFiles
BundleA folder under templates/_shared/ that several modules include (LaTeX class files, the Lean installer). Flattened to the workspace root.shared_bundles
SkillA file at skills/<id>/SKILL.md with a name and description in frontmatter. The agent lists every skill in the workspace and reads one when it applies.agent-ts/src/prompt/skills.ts
Selection actionA button over selected text that runs a prompt with a skill, answering in a chat or as a comment thread on the selection.selection_actions, lib/assistants/selection-actions.ts
VerifierA registered check (compile, Lean, claim, audit) a module switches on. Its own contract: Verification.verifies, lib/workspace/verifiers.ts
KindThe word the gallery shows: template, verifier, skill, or assistant. Derived from the manifest, never declared.kindOf in app/modules/discover-model.ts
Create fromA new workspace seeded with the module.POST /api/templates/new
ConnectAdd the module to a workspace that already exists.POST /api/workspace/assistants/connect
CatalogThe DB copy of every manifest that the gallery and the APIs read, synced from the repo on deploy.assistants table, syncAssistantsToDb

Code map: lib/templates/registry.ts (manifest schema, discovery, file resolution) · templates/ (the modules) · lib/assistants/connect.ts (the connect planner) · app/api/workspace/assistants/connect/route.ts (connect) · app/api/workspace/modules/route.ts (list, remove) · app/api/templates/new/route.ts (create from) · lib/assistants/store.ts (catalog reads, repo fallback) · lib/assistants/sync.ts (deploy sync) · app/modules/discover-model.ts (kind, featured, field rows) · agent-ts/src/prompt/compile.ts (what the agent sees) · tests/templates/registry.test.ts (the gate).

1. What a module is

A module is a folder of files plus a short manifest. That is the whole mechanism: no plugin code, no UI, no registration table. Adding one to a workspace seeds its files at the workspace root, appends its instructions to the agent's system prompt, and, when the manifest says so, installs a button over selected text or switches on a check. Removing it takes the instructions, buttons and checks away and leaves the files, because they are the user's now.

Everything a module can do is one of four things:

  • Seed files. A paper's main.tex and class files, a protocol in AGENTS.md, a script the agent runs, a starter question.md.
  • Instruct the agent. system_prompt_addendum joins every chat's system prompt. AGENTS.md (if shipped) is read at the start of every turn. A skills/<id>/SKILL.md is listed to the agent and read on demand.
  • Run a check on selected text. A selection action: prompt, skill, the check it runs (verifier), and where the answer lands.
  • Switch on a check. A verifier id in verifies; the check itself lives in code, see Verification.

A few modules are built in rather than installed — the LaTeX compiler, daily notes — and live in code (lib/workspace/verifiers.ts, components/workspace/daily-notes-card.tsx), not under templates/: they take no manifest and no Modules row (the compiler just runs; daily notes has its switch in Settings → Workspace).

What a module cannot do, so you do not go looking: declare a schedule (the instructions can tell the agent to create one with schedule_create when the user asks), add a palette command, shortcut, sidebar row or view (those are hard-coded in the shell), substitute dates or names into seeded files (files are verbatim), or hide a file from other members (the workspace is the privacy boundary, Privacy and security).

2. The smallest module, in five steps

A skill module: one manifest, one skill. Ten minutes.

  1. Make the folder. Category research for a method the agent follows, project for a workspace with a toolchain, loop for a never-ending agent loop, a LaTeX category for a paper starter (§3).

    templates/research/line-edit/
      manifest.yaml
      files/
        skills/line-edit/SKILL.md
    
  2. Write the manifest. Five fields are required; intro_message is required too outside conference and journal (the test checks).

    slug: line-edit                # must equal the folder name
    name: Line edit
    category: research             # must equal the parent folder
    description: >-
      A careful line-by-line editing pass that lands as reviewable
      suggestions. Grammar, tightening, and the author's voice kept.
    fields:
      - writing
    intro_message: >-
      Ask me to line-edit any document here. Every change arrives as a
      suggestion you accept or reject; I never rewrite silently.
    suggested_prompts:
      - Line-edit the open document
    system_prompt_addendum: |
      When asked to edit, tighten or proofread prose, read
      `skills/line-edit/SKILL.md` first and follow it. Make every change
      as a suggestion, never a silent rewrite.
    
  3. Write the skill. Frontmatter name and description are what the agent sees before it decides to read the body, so the description says when the skill applies.

    ---
    name: Line edit
    description: A line-by-line editing pass over a document, landing as suggestions. Use when asked to edit, tighten, or proofread without changing meaning or voice.
    ---
    
    Read the whole document first, then edit sentence by sentence.
    
    1. Fix grammar, spelling, and punctuation.
    2. Tighten: cut filler, collapse redundancy, split run-ons.
    3. Preserve the author's voice and meaning exactly.
    4. Finish with the two or three recurring habits you fixed most.
    
  4. Run the gate and bump the count.

    pnpm test templates/registry
    

    tests/templates/registry.test.ts pins how many listed modules each category has; raise count('research') by one. Everything else in that file (description length, intro present, locked files shipped, skill bundles text-only) checks itself.

  5. Try it. With a dev server running, open /modules/line-edit and click Use this module, or create a workspace from a shell:

    curl -sS -X POST http://localhost:3000/api/templates/new \
      -H 'Content-Type: application/json' -H 'Cookie: sd_anon=trythisnow123456' \
      -d '{"slug":"line-edit"}'
    

    The gallery list at /modules is read from the catalog (§11), so an unmerged module is missing from the list but its own page and both APIs work through the per-slug repo fallback.

Then open a PR against dev. The deploy syncs the catalog and the module is live on dev.sundial.md.

3. Layout and categories

templates/
  _shared/<bundle>/      Files several modules include (acmart, lean4-setup, …).
                         Every file lands at the workspace root. SOURCE.txt
                         records provenance and is never seeded.
  <category>/<slug>/
    manifest.yaml
    files/               Seeded verbatim, nested paths kept.

The category is the folder, and the manifest must agree (category: equals the parent folder, slug: equals the folder name; discovery throws otherwise). Categories are the list TEMPLATE_CATEGORIES in lib/templates/registry.ts, mirrored in agent-ts/src/prompt/templates.ts:

CategoryForRules
conferenceA venue's official style, one per venue and year (icml-2026).LaTeX. Addendum must carry live guideline URLs; the agent gets a fetch-the-guidelines preamble.
journalA journal's style (jmlr, elsarticle).LaTeX. Same as conference.
thesisUniversity theses.LaTeX. Needs intro_message.
classicPreprints, letters, notes, Tufte (arxiv-preprint).LaTeX. Needs intro_message.
talkBeamer decks and posters.LaTeX. Needs intro_message.
cvCVs and resumes.LaTeX. Needs intro_message.
loopA never-ending agent loop: AGENTS.md protocol, goal.md, tasks.md, attempts.md (goal-loop).Needs intro_message. Locks its rules.
hackathonA skills pack for one event (reagent).Needs intro_message.
projectA workspace with a toolchain the sandbox installs on first use (lean4, wikiskill), or a capability with no toolchain at all: an AGENTS.md protocol and nothing else.Needs intro_message. A toolchain means a setup_command.
researchA method the agent follows: skills, a starter doc, a check (math-research, claim-verification).Needs intro_message.

The LaTeX categories (LATEX_TEMPLATE_CATEGORIES) owe a main.tex, a prebuilt main.pdf and its main.synctex.gz (§9). Every other category is free-form. Prefer an existing category: a new one touches TEMPLATE_CATEGORIES, the brain's copy, the count pins in the registry test, and, for documents, DOCUMENT_CATEGORIES in app/modules/discover-model.ts.

4. The manifest

Hand-rolled validation in parseTemplateManifest, not Zod; every rule below throws at discovery, so pnpm test templates/registry is the check. Limits are exact.

FieldTypeWhat it doesRule
slugstringIdentity everywhere: folder, URL (/modules/<slug>), API, projects.template_slug.Required. ^[a-z0-9-]+$. Equals the folder name.
namestringCard title, the ## <name> heading of its instructions section, the sidebar row.Required.
categorystringThe folder; picks the agent preamble and the gallery kind.Required. One of the table above. Equals the parent folder.
descriptionstringThe card and the module page opener. Rendered as plain text.Required. 40 to 200 characters, no backticks or backslashes.
system_prompt_addendumstringAppended to the system prompt of every chat in a workspace that has the module (§6).Required. Conference and journal addenda must contain an https:// URL.
intro_messagestringSunny's first message in a workspace created from the module; also posted on connect when the caller names a chat. A seeded assistant message, rendered as markdown like any reply: backticks and lists are fine, keep it short.Required outside conference and journal (their greeting is built from the venue).
suggested_promptsstring[]Chips on the empty chat's hero.At most 4, each at most 80 characters.
open_pathstringThe file a fresh workspace opens on (?filePath=), and the file the sidebar opens after connect. Without it, arrival falls back to the most recently edited file, and a LaTeX-category module gets the tex tour (LATEX_TEMPLATE_CATEGORIES gates it in app/api/templates/new/route.ts and lib/templates/anon-template-workspace.ts).Must be a shipped path. One value.
fieldsstring[]Card tags and search terms. Only a value in FIELD_ROWS (app/modules/discover-model.ts) places the module in a gallery row: math; cs, ml, systems, pl, hci, security; physics; biology, medicine, medical, neuroscience, chemistry; writing, publishing, academic. Any other value is a tag only; add a row there when a new area needs one.Lowercase letters.
sources{name, url}[]Links on the card and the module page: where the class files, tools or papers come from.
licensestringLicense of vendored class or style files.Reviewers check it before a bundle lands in _shared/.
shared_bundlesstring[]_shared/<name> folders to include.On a path collision files/ wins.
locked_filesstring[]Seeded paths agents may never edit (files.is_locked). Humans edit freely and can unlock from the tree.Every path must ship.
setup_commandstringSandbox bash fired at create and connect, before the first question (§8).Idempotent. Trimmed; empty means none.
default_modelstringSeeded into projects.agent_default_model: the model every chat without its own falls back to.Must be in ALLOWED_GATEWAY_MODELS (lib/workspace/model-allowlist.ts). Paper templates declare none.
selection_actionsobject[]Buttons over selected text (§7).At most 8.
verifiesstring[]Verifier ids the module switches on.Each must be registered (getVerifier).
unlistedbooleanHidden from every list and the gallery, still resolvable by slug so existing workspaces keep working.Retire a module this way, never by deleting it.
year, deadline, page_limit, anonymizedscalarPaper-template facts shown on the card.Optional.

A selection action:

FieldRule
idRequired, kebab-case, unique within the manifest.
labelRequired, at most 24 characters. The action's short name (a check's button wears the check's registry name).
titleRequired, at most 64 characters. The title of the chat a run starts.
skill_idRequired, kebab-case. skills/<skill_id>/SKILL.md must ship, and every file under that folder must be text and use paths relative to the folder (the literal skills/<skill_id>/ may not appear inside them, because connect can rename the folder; §5 has the convention). The manifest's addendum and action prompts may name skills/<skill_id>/… because connect rewrites them (remapAssistantPaths).
promptRequired, at most 8000 characters. Sent with the selected text.
resultchat (default: a fresh chat) or thread (a comment thread anchored on the selection).
enabled_by_defaultLegacy, no effect: an installed action is always on (there is no switch).
modelThe run chat's model.
verifierA registered verifier id: the action produces that verifier's verdict, in the annotation lane, with its result instruction appended.

5. The files

resolveTemplateFiles walks files/ after every bundle in shared_bundles, so a path in files/ overrides the bundle's copy. What it skips: names starting with a dot, SOURCE.txt, LaTeX build artifacts (.aux, .log, .out, .toc, .bbl, .blg, .fls, .fdb_latexmk) and docstrip sources (.dtx, .ins). Everything else ships, so a __pycache__/ you left behind ships too, as blobs. Run shipped Python with sys.dont_write_bytecode and never py_compile in the tree.

Text or blob is decided by lib/sync/policy.json (isBlobFile): a blob extension is read as base64 and seeded on the blob rail, everything else is a live document with history. Mime comes from the extension (text/markdown, application/x-tex, application/pdf, …).

Among text files, markdown (markdown_extensions in the same policy: .md, .mdx, .markdown) is parsed by the one codec (lib/markdown/parser.mjs + lib/crdt-js/markdown_yjs.mjs) and re-serialized canonically on every persist. Every other text extension (.json, .py, .tex, …) is stored byte for byte. So keep machine-written state (JSON, logs) in a non-markdown extension, and if a module puts markers in notes (a wikilink, a :: separator, a lone ? line), prove they survive with populateFromMarkdown + serializeDoc in a test beside the others in tests/collab/.

Files are verbatim. There is no placeholder syntax: no {{date}}, no {{title}}. If a file must name today or the user, the agent writes it on the first turn; say so in the addendum.

Paths that mean something:

  • AGENTS.md: read by the agent at the start of every turn (root only; nested AGENTS.md files are ignored by design). The brain cuts anything over 20,000 characters to the first 14,000 and the last 4,000. Lock it when it is a protocol the agent must not rewrite (goal-loop). On connect into a workspace that already has one, its content is appended as a ## <name> section (§10), so write the body without a top heading.
  • skills/<id>/SKILL.md: listed to the agent under ## Skills as its name and description, read on demand. Supporting files (references/*.md, scripts/*.py) sit beside it. Keep links relative. Refer to a sibling as <this-skill-folder>/scripts/x.py and tell the agent to resolve it from the SKILL.md path it read (claim-verification is the pattern). Connect may rename the folder to skills/<id>-<slug>/ but keeps its depth (suffixedPath), so a script that needs the workspace root derives it from its own location, Path(__file__).resolve().parents[3] for skills/<id>/scripts/x.py, and takes a --root override for tests.
  • main.tex with main.pdf and main.synctex.gz: the paper and its prebuilt preview (§9).
  • setup.sh: what setup_command: bash setup.sh runs (§8).

A user can also bring a skill without a module: Modules → Add skill takes a GitHub or GitLab SKILL.md URL or a .skill zip (POST /api/workspace/skills, lib/skills/). Same file, same folder, no manifest. Make a module when the skill needs starter files, a button, a check, or a place in the gallery.

The Modules section lists every skills/<id>/SKILL.md in the workspace as a row, by its frontmatter name and description (skills in GET /api/workspace/modules, read from the workspace's files; a ?pshare= guest sees only what the link covers). Three things happen from the rows:

  • New module writes skills/<kebab-id>/SKILL.md with the frontmatter and the §2 body shape (lib/skills/new-module.ts) through the Add-skill route, which suffixes a taken id, and opens it.
  • ⋮ → Share opens the share window scoped to the folder skills/<id>/. Nothing is special about the link: it is the one minter (lib/workspace/share-links.ts), a folder grant, and an agent that gets it reads the SKILL.md and follows it (/start says so).
  • Add to my workspace, shown to a visitor who cannot edit here, copies the folder into a workspace of theirs: POST /api/workspace/modules/import {fromWorkspaceId, path, pshare, toProjectId} reads the source through the scoped access the link grants and seeds with planConnectFiles rules (free path as-is, identical skipped, taken name → skills/<id>-copy/, a taken suffix → 409, never half-installed).

On the desktop the same section renders on a local folder: the rows are the folder's skills/*/SKILL.md (a check module's skill, claim-verification, out-of-date or lean-blueprint, is drawn as its check row instead, folderChecks, saying FOLDER_CHECK_WORDS), New module writes one, and + Add copies a catalog module's files in with no sign-in — the public GET /api/templates/<slug>/files plus the sidecar's POST /projects/<id>/modules {slug}, which lands the files with the same planner rules through the file path every save takes. Modules that ship no files (seedsFiles in the listing) are not offered there.

6. What the agent sees

At create and at connect the addendum is copied into the workspace row (projects.template_addendum_default, one ## <name> section per module, appendAssistantAddendum), so a later edit to the manifest never changes a workspace behind its back. The brain reads that column each turn:

SourcePrecedence
projects.template_addendum_overrideFirst. Settings → Instructions, the user's own text.
projects.template_addendum_defaultSecond. The frozen module sections.
The manifest on disk (agent-ts/src/prompt/templates.ts)Last, for workspaces older than the column.

It lands in the system prompt as ## Template Instructions. For conference and journal a preamble precedes it: fetch the live author guidelines before editing a paper file and follow them exactly; other categories get the addendum verbatim, so their rules must be self-contained.

Also in the prompt, from the workspace itself: the root AGENTS.md (and CLAUDE.md), the ## Skills list of every skills/*/SKILL.md, the names of workspace secrets, and a runtime tail with the user's timezone (user_preferences.timezone of the workspace owner, UTC when unset) and local_datetime. Write instructions against these facts: "today is the local date from Runtime" works; "ask the user their timezone" does not.

The intro is a seeded assistant message, not a model turn: intro_message is Sunny's first message in a fresh workspace (seedAssistantMessage), and connect posts it into the chat named by announceChatId when one is given. Suggested prompts render as chips only on an empty chat and send a normal turn when clicked, so they must be asks the agent can answer, not UI commands.

7. Selection actions

A selection action runs on selected text. The text bubble shows only checks: an action that names a verifier whose registry spec has onSelection is that check's button (in the check's own words), on the files the check applies to. An action with no verifier has no button; the agent still has its skill, so a person asks for it in chat. The run is an ordinary chat turn: the action's prompt, the selected text, and the skill at skills/<skill_id>/SKILL.md as the method. With result: thread the answer becomes a comment thread anchored on the selection, which is how a verifier's verdict sits beside the line it judged; with a verifier the thread is authored as that verifier and its result contract is appended to the prompt. The definition is snapshotted into the workspace at install (workspace_selection_actions), so editing the manifest later does not change a button collaborators already use.

selection_actions:
  - id: verify-claim
    label: Verify                 # ≤ 24 chars, the short name
    title: Claim Verifier         # ≤ 64 chars, the run chat's title
    skill_id: claim-verification  # files/skills/claim-verification/SKILL.md
    result: thread
    verifier: claim
    enabled_by_default: true
    prompt: >-
      Verify every checkable claim in the selected passage …

What the run receives (invocationContent in app/api/workspace/assistant-actions/invoke/route.ts), four parts joined by blank lines:

> _from `notes/week-3.md`:_
> the selected text, one quoted line per line

Read `skills/<skill_id>/SKILL.md` first and follow it.

<prompt>

<the verifier's result instruction, when `verifier` is set>

A result: thread action is a one-click check with a card waiting, so its run gets what it needs up front instead of the skill line: the paragraph around the selection, whether the document has a bibliography, and "skip the skill file". Its chat runs on the action's model too.

So the prompt may rely on the quoted path ("append to the note the selection came from"). With result: chat the run starts a fresh chat, so nothing else from the editor is in context. GET /api/workspace/assistant-actions?projectId= lists the actions a workspace has installed; POST /api/workspace/assistant-actions/invoke is what the button calls.

claim-verification is the worked example: one action, one skill, one verifier. lean-blueprint adds a model and no enabled_by_default. A generation button is the result: chat shape: its prompt appends cards or notes to the quoted file.

8. Setup commands

setup_command runs in the sandbox (Modal, one per Sunny, the workspace volume at /workspace), fire-and-forget, at workspace create and at connect, in the sandbox of the most recent chat (warmAssistantSetup). The agent may run it again on its first turn. So:

  • Idempotent, and cheap when nothing is left to do. Take a lock (flock on a file under /workspace/.sundial/) so the warmup and the first turn do not race; wikiskill's setup.sh is the pattern.
  • Persist under /workspace (a venv, a toolchain); the sandbox idles down after 15 minutes and the volume is what survives.
  • Budget for the first turn: Bash calls time out at 600 seconds. A long install is fine in the warmup, but the first turn must not block on it; cap any probe with a timeout and print the decision-relevant lines first.
  • Say in intro_message that the first run installs something and how long it takes.

Combined workspaces warm only the first setup-bearing module.

What a shipped script can rely on, whether the setup runs it or the agent runs it with Bash:

  • The shell's cwd is the workspace root (/workspace), and the notes on disk are live: the sandbox materializes every doc-store edit to disk as it lands, so a script reads the current text without a fetch.
  • Every write under /workspace is mirrored back into the workspace by the sandbox tree watcher (WorkspaceTreeWatcher in sandbox/file_server.py, through sandbox/shared/crdt_sync.py): text extensions become live documents, blob extensions go to the blob rail, and text over 10 MB mirrors as a blob. ignored_paths in lib/sync/policy.json (.sundial/, __pycache__/, tmp/, .venv/, *.pyc, …) never mirror, so scratch and locks go there.
  • A write made during a Bash call lands attributed to that turn (actor='agent', the turn and tool call ids), so it shows as a diff card and, in a suggest-mode chat, as a suggestion. Markdown is canonicalized on the way in (§5). If an edit should read as the agent's own reviewable change rather than a script's rewrite, have the agent make it with its edit tools and let the script only read.

9. Paper templates

A LaTeX module ships main.tex, the class or style files it needs (from a bundle when several venues share them), a references.bib, and a prebuilt main.pdf plus main.synctex.gz beside the source. The PDF makes the preview render the moment the workspace opens instead of waiting out a cold compile; the SyncTeX index keeps double-click inverse search alive, because seeding the PDF suppresses the arrival compile that would otherwise produce it. The registry test refuses a listed LaTeX module without both.

The authoring loop:

pnpm templates:local <slug>     # local latexmk, same engine rule as the pool
pnpm templates:compile <slug>   # through a dev server and the Modal compile pool
pnpm templates:pdfs <slug>      # rebuild files/main.pdf + main.synctex.gz
pnpm templates:thumbs <slug>    # public/templates/thumbs/<slug>.webp for the card

Compile-pool facts: bibtex only (no biber), no binary starters apart from the PDF pair (figures go in TikZ), LaTeX kernel 2021-11 (no \DeclareKeys), and a class that forces \pdfoutput=1 needs the \ifdefined\XeTeXversion\PassOptionsToPackage{xetex}{hyperref}\fi guard before \documentclass (see journal/lmcs). Templates that typeset \today show the build date until the first recompile. Bump vendored_at in the bundle's SOURCE.txt when you refresh its files.

10. Lifecycle: create, connect, list, remove

Create from. POST /api/templates/new with {"slug":"<slug>"} seeds a new workspace: files at the root, intro_message as the first message, open_path as the file to open, default_model on the workspace, setup_command warming, the addendum frozen into the row. Up to four modules combine with {"slugs":[…],"layout":"main"}: the first is the workspace's identity and sits at the root, the rest nest under <slug>/. Auth is a Bearer token or the anonymous sd_anon cookie (the /start skill covers both). Humans get the same through /modules/<slug> → Use this module, /new?template=<slug>, and /t/<slug>, a copy of /start with the first call pre-filled for one module, made for handing to a room of agents.

Connect. POST /api/workspace/assistants/connect with {"projectId","slug"} (owner or editor; optional announceChatId, enableActionIds) adds a module to an existing workspace. The planner (planConnectFiles) is pure and has three rules per path:

  1. Path free: seed as-is.
  2. Path taken with identical content: skip, and inherit the module's lock if it declares one.
  3. Path taken with different content: AGENTS.md gets the module's content appended as a ## <name> section (mergeAgentsFile, a no-op when the section is already there); every other file gets a -<slug> suffix before its first dot, and a skill folder becomes skills/<id>-<slug>/. Files that belong together (main.tex, main.pdf, main.synctex.gz) rename as a group so the preview stays attached. A suffixed name that is itself taken by different content is a 409: the module is not half-installed.

Then the addendum section is appended (idempotent), paths inside the addendum, the action prompts and the setup command are rewritten to any suffixed names, the actions are snapshotted, the setup command is warmed, and the intro is posted if a chat was named. The response:

{"connected":{"slug":"…","name":"…","announced":false,
  "added":["skills/x/SKILL.md"],"renamed":{},"identical":[],
  "agentsMerged":false,"openPath":"question.md",
  "selectionActions":[…],"formalizable":null}}

Reconnecting is a repair, not an upgrade: an installed module keeps its frozen instructions.

List. GET /api/workspace/modules?projectId= returns the module the workspace was created from plus every connect: {"modules":[{"slug","name","category","description","skills","verifies"}]} (skills: the skill folders it seeded, which the rail lists under it). The sidebar Modules section and the verifier registry read this, never a slug list. GET /api/workspace/assistant-actions?projectId= lists the installed selection actions (§7).

Remove. DELETE /api/workspace/modules with {"projectId","slug"} drops the install row, the selection actions and the module's section of the instructions. Files stay. Schedules the agent created stay too, so a module whose instructions arm one should tell the agent what "stop" means.

The card at /modules and the page at /modules/<slug> come from the manifest: name, description, fields as tags, sources as links, the thumbnail when public/templates/thumbs/<slug>.webp exists, and the file inventory grouped by role (skill, starter, library, doc, setup, prebuilt). The kind word is derived by kindOf: a verifier when the slug is a verifier's module, a template for the LaTeX categories, an assistant when it has selection actions, a skill when it seeds skills/, a template otherwise. KIND_OVERRIDE in app/modules/discover-model.ts is the escape hatch for the few the rule misreads (goal-loop is an assistant with no actions). FEATURED there is the goal-first "Start here" row; it is optional, per-slug, and edited by hand.

The list is the catalog: the assistants and assistant_files tables, written from the repo by pnpm assistants:sync (syncAssistantsToDb), which the dev and staging deploys run after migrations. Once the table has rows it wins wholesale; the repo registry is the fallback for an environment that never synced and, per slug, for any module the table lacks. Two consequences: an unmerged module is absent from the local list but present at /modules/<slug> and in both APIs; and never run the sync locally against the shared dev database for unmerged work. verifies and suggested_prompts are read from the repo manifest even for catalog rows. Retire a module with unlisted: true or status: blocked, never by deleting the folder, which would let the fallback resurrect it.

Popularity (assistant_popularity) orders the gallery by workspaces created from or connected to each module.

12. Tests and proof

Narrowest first, the full gate once before the PR.

WhatCommand
The manifest and filespnpm test templates/registry. Bump the category count.
Connect behaviour you changedpnpm test templates/assistants-connect
The gallery kind, if you touched KIND_OVERRIDEpnpm test modules-discover
A shipped Python scripttests/skills/<name>_test.py, run by pnpm test:unit:python (python3.11, the whole suite; tests/sandbox in it needs infra and is not a module author's gate). Narrow: PYTHONPATH=. PYTHONDONTWRITEBYTECODE=1 python3.11 scripts/dev/test-python.py discover -s tests/skills -p "<name>*_test.py" -t . (the directory must have an __init__.py; prefix PYENV_VERSION=3.11.13 when python3.11 is a pyenv shim). Fixture-driven; sys.dont_write_bytecode = True.
A LaTeX modulepnpm templates:local <slug>, then pnpm templates:compile <slug> against a dev server.
This pagepnpm test module-sdk-doc
Everythingpnpm test:full

Live proof, in a real workspace: npm run dev, open /modules/<slug>, Use this module, and drive the first turn the intro promises. A module that installs a toolchain gets its first live turn on the shared dev stack (the sandbox is Modal, not your machine); a first turn that "times out" on a fresh sandbox usually means the warmup and the turn serialized on the setup lock, so rerun the idempotent setup rather than debugging the toolchain. Verify seeded content with GET /api/workspace/files/download?projectId=<id>&path=<path>.

From a shell, with the same cookie or token on every call: POST /api/templates/new returns project.id and project.initialChatId; check the seed with the download route; send the first turn with POST /api/workspace/messages and {"chatId":"<initialChatId>","content":"<a suggested prompt>"}; poll GET /api/workspace/messages?chatId=<id>&afterSequence=<n> for the reply; then download the files the turn should have changed. The turn runs on the shared dev brain and spends a live model turn, and a cold sandbox adds about a minute before the first Bash call returns.

For a module whose value is visible (a paper that compiles, a check that lands as a thread, a loop that runs), record the real flow on dev and put the recording in the PR (pnpm pr-visuals:upload).

13. For agents: the sequence

You were asked to add a module. Do this, in order, and nothing else.

  1. Read this page. A skills module needs §1, §2, §4 to §8, §10 and §12; a paper template adds §3, §9 and §11 (§11 otherwise only explains why an unmerged module is missing from /modules). Then read one sibling of the same shape: research/math-research for a skills module, research/claim-verification for a button and a check, project/wikiskill for a toolchain with a locked protocol, loop/goal-loop for a loop, conference/icml-2026 for a paper.
  2. Pick the category from §3. Do not add a category.
  3. Create templates/<category>/<slug>/manifest.yaml and files/. Slug and category equal the folders. Every required field, intro_message included. Description 40 to 200 plain characters. Suggested prompts are asks, at most four.
  4. Put the method in files/skills/<id>/SKILL.md with frontmatter, and tell the agent in system_prompt_addendum when to read it. Put rules that must survive a missing skill in the addendum itself. Put a per-turn protocol in AGENTS.md, without a top heading, and lock it if the agent must not rewrite it.
  5. Ship any script the agent runs under files/, with a test under tests/, and make it idempotent. If it needs installs, add setup_command: bash setup.sh with a lock and a fast no-op path.
  6. Never write a placeholder into a file, never add UI, never define a second parser or catalog. If the module needs something on a schedule, write the instruction that has the agent create the schedule when the user says so, with the user's timezone from Runtime.
  7. Run pnpm test templates/registry, raise the category count, run pnpm test:full once.
  8. Prove it live on a dev server: /modules/<slug>, Use this module, the first turn. Fix what the intro promised and the turn did not do.
  9. Open a PR against dev. No review is required on dev; the deploy syncs the catalog.

If this page did not answer a question you had, the answer belongs on this page: add it in the same PR.

14. Worked examples

ModuleShapeRead it for
research/math-researchFour skills, a starter question.md, open_path, a self-contained addendum with hard rules.A skills module.
research/claim-verificationOne skill, one selection action (result: thread, verifier: claim, on by default), verifies: [claim]; scripts/*.py beside SKILL.md, referenced as <this-skill-folder>/scripts/x.py.A button and a check; a skill that ships scripts.
research/lean-blueprintmain.tex plus the lean4-setup bundle, setup_command, verifies: [lean, audit], a Formalize action with its own model.A paper kept in step with a proof.
project/lean4unlisted: true, kept resolvable for existing workspaces.Retiring a module.
project/wikiskillLocked AGENTS.md, check.py and task data; setup.sh with flock; default_model pinned to a small model.A toolchain with a protocol.
loop/goal-loopLocked AGENTS.md and goal.md, default_model, an addendum with invariants that hold even without the files.A never-ending loop.
hackathon/reagentFour tool skills, sources with key links, an intro that says what needs a key.A skills pack for an event.
conference/icml-2026Class files from a bundle, prebuilt main.pdf and main.synctex.gz, guideline URLs in the addendum, year, deadline, page_limit, anonymized.A paper template.

15. Code map

ConcernWhere
Manifest schema, categories, validationlib/templates/registry.ts (parseTemplateManifest, TEMPLATE_CATEGORIES, LATEX_TEMPLATE_CATEGORIES)
File resolution, bundles, lockslib/templates/registry.ts (resolveTemplateFiles, validateTemplateFiles, applyLockedFiles)
The brain's copy of the categories and the manifest fallbackagent-ts/src/prompt/templates.ts
What the agent seesagent-ts/src/prompt/compile.ts (templateSection, skillsSection, runtimeSection), agent-ts/src/prompt/load-context.ts
Create fromapp/api/templates/new/route.ts, lib/workspace/bootstrap.ts
Connect planner and instruction mergelib/assistants/connect.ts (planConnectFiles, mergeAgentsFile, appendAssistantAddendum, removeAssistantAddendumIfPresent)
Connect, list, removeapp/api/workspace/assistants/connect/route.ts, app/api/workspace/modules/route.ts
Setup warmuplib/assistants/warmup.ts (warmAssistantSetup)
Sandbox mirror for scriptssandbox/file_server.py (WorkspaceTreeWatcher), sandbox/shared/crdt_sync.py, agent-ts/src/tools/bash.ts, lib/sync/policy.json
Selection actionslib/assistants/selection-actions.ts, app/api/workspace/assistant-actions/route.ts (list), app/api/workspace/assistant-actions/invoke/route.ts (run, invocationContent)
Verifierslib/workspace/verifiers.ts (registerVerifier), Verification
Catalog reads and the repo fallbacklib/assistants/store.ts (listAssistants, getAssistant)
Catalog sync on deploylib/assistants/sync.ts (syncAssistantsToDb), scripts/assistants-sync.ts, .github/workflows/deploy.yml
Gallery kind, featured row, field rowsapp/modules/discover-model.ts (kindOf, KIND_OVERRIDE, FEATURED, FIELD_ROWS), app/modules/listing.ts
Module pageapp/modules/[slug]/page.tsx, app/modules/[slug]/detail-model.ts
Bring-your-own skillapp/api/workspace/skills/route.ts, lib/skills/
Sidebar Modules sectioncomponents/workspace/modules-section.tsx, components/workspace/modules-gallery.tsx
Agent-facing entry points/start (content/sundial.SKILL.md), /agent-docs, /t/<slug>, /api/templates
LaTeX toolingscripts/dev/compile_template_local.mjs, scripts/dev/check_templates_compile.mjs, scripts/dev/build_template_pdfs.mjs, scripts/dev/build_template_thumbs.mjs
The gatetests/templates/registry.test.ts, tests/templates/assistants-connect.test.ts, tests/ui/modules-discover.test.tsx, tests/ui/module-sdk-doc.test.ts