Directory contract and discovery
The shape of a Neuralis package and exactly what file-first discovery scans.
A Neuralis package is an npm-style directory with a manifest and a set of
file-first declarations. Nothing a package contributes is registered in code
against the host — the host discovers everything from the package's files at
load time and merges the result into the package's PackageDefinition.
Directory contract
my-package/
package.json # manifest: the `neuralis` block declares the package
tools/ # *.json — strict JSON-Schema tool definitions
workflows/ # *.json — declarative workflow templates
skills/ # <name>/SKILL.md — folder-per-skill bundles
instructions/ # *.md — system-prompt instruction contributions
rules/ # *.md — always-on rule contributions
agents/ # *.md — delegate agent personas
docs/ # *.md — reference docs, listed in the overview + read on demand (not injected)
commands/ # *.json — prompt/slash command definitions
hooks.json # declarative lifecycle hooks
team/ # <slug>/member.json + identity/*.md + files/**/*.md
src/ # runtime code (lifecycle, routes, tool handlers)
dist/ # compiled runtime output (node) or package.wasmOnly package.json is required. Everything else is optional — a purely
declarative package can ship nothing but tool schemas and markdown.
A package that ships runtime code is ESM: set "type": "module" in
package.json, compile with module/moduleResolution: "NodeNext" and
target: "ES2022", and give every relative import an explicit .js
extension (a directory import resolves to /index.js). The compiled dist/
output is what the runtime loads, resolved through the package exports map.
(The WASM project-package path is separate — its sandboxed guest bundle is
emitted as CommonJS for the QuickJS runtime; see
WASM build.)
What discovery scans
| Path | Becomes | Notes |
|---|---|---|
tools/*.json | PackageTool[] | Tool name = filename without .json. Invalid schemas fail the package load. |
workflows/*.json | PackageWorkflow[] | Workflow templates — a parallel typed JSON array like tools, never prompt-injected. Template id = filename; invalid templates fail the package load. See Workflow templates. |
skills/<name>/SKILL.md | skills array (category: 'skill') | Folder-per-skill; the walk is recursive under skills/. |
instructions/**/*.md | instructions array (category: 'instruction') | Arbitrarily named, recursive. |
rules/**/*.md | rules array (category: 'rule') | Arbitrarily named, recursive. |
agents/**/*.md | agents array (category: 'agent') | Delegate personas. |
docs/**/*.md | docs array (category: 'docs') | Reference material. |
commands/*.json | PackageCommand[] | Each file must carry an id. Commands are served as static prompt text to external protocol clients — there is no executor, and declared arguments are not substituted into the body. Put anything that must run in a skill instead. |
hooks.json | PackageHook[] | Seven events are recognised — PreToolUse, PostToolUse, PostToolUseFailure, SessionStart, SessionEnd, Stop, PrePrompt — and an entry naming anything else is skipped. Per-event semantics are validated at load too: PostToolUse may only observe, Stop only forceContinue, PrePrompt only inject. Invalid entries are skipped, never crash bootstrap. Only the in-process callback kind executes today: a script or http handler validates and then fails at dispatch with a no-executor error. |
team/<slug>/member.json | TeamMemberContribution[] | Plus identity/*.md and files/**/*.md siblings. The slug must be a single safe path segment, and every discovered or declared template path stays inside the package: relative, no .. or . segment, no absolute prefix, no backslash, no control character, at most 512 characters, at most 64 entries per list. A manifest that breaks this is rejected at load and the package registers nothing — these paths become a file read performed by the platform itself, so they are validated before the read, and resolved against the package root again at provision time. Rejection is a registry outcome rather than a quarantine: markdown inside a directory that is also synced as a source is still discovered as a source package, which reads no manifest. |
src/routes/*.ts | Route handlers | Authored the same for node builtins AND WASM project packages; WASM builds emit dist/routes.json for the host feature-gate. See Routes. |
src/tools/*.ts | Tool handlers | Default-export handler, paired with tools/<name>.json by filename. |
src/channels/*.ts | First-party realtime channels | Export matching channel, feature, and subscribe; compiled modules feed the shared events hub. |
src/events/*.ts | First-party event sources | Export matching subject, a required visible, and an optional bridge; see Manifest → events. |
src/runtimeProvider.ts | Runtime provider | A first-party provides: ["runtime"] package exports the named runtimeProvider. |
src/lifecycle.ts | Lifecycle module | See Lifecycle. WASM init is lazy/cached. |
Two additional skill paths exist for agentskills.io import compatibility:
<root-subdir>/SKILL.md— a skill package whose skills sit directly under the package root instead ofskills/. Reserved directories (src,dist,tools,node_modules, the category folders, …) are never treated as skill folders.<root>/SKILL.md— a bare single-skill folder with nopackage.jsonat all: the package root is the skill, and${SKILL_DIR}resolves to it.
What is NOT scanned
- Package-root loose
.mdfiles.README.md,AGENTS.md,PLAN.md, and any other markdown at the package root are package-meta files, never contributions. The only root-level exception is a rootSKILL.md. - Anything outside the category folders. There is no generic
files/contribution directory and no cross-folder contribution nesting — a rule must live underrules/, an instruction underinstructions/, and so on.
Category comes from the folder
Every discovered markdown file carries a required category
('skill' | 'instruction' | 'rule' | 'agent' | 'docs') set authoritatively
from its source folder — never from frontmatter. A role: frontmatter key is
ignored with a warning (role denotes the user rank elsewhere in the system),
and a category: key is not read at all. One exception re-categorizes a file: a
skill that declares context: fork in its frontmatter registers as an agent
(a skill-fork delegate persona).
Flat contributions (instructions/, rules/, agents/, docs/) — and any
*.agent.md — need a frontmatter name (or id) to be discovered; a bare
markdown file without one is skipped silently. The exceptions are the forms
whose filename is the identity by protocol: skills/<name>/SKILL.md (the skill
id falls back to the folder name), commands/*.md, the identity basenames
(AGENTS.md, CLAUDE.md, CLAUDE.local.md, GEMINI.md,
.github/copilot-instructions.md), .cursorrules, .windsurfrules and
.cursor/rules/*.mdc. The same admission rule applies to a package drop and to
a synced source, so the three package
forms never disagree about what a contribution is.
Skill bundles — the directory is the scope
A skill's scope is its whole directory, not just the SKILL.md entry file.
At scan time discovery enumerates the bundle into PackageFile.files[] —
dir-relative paths ordered SKILL.md first, then scripts/**,
references/**, assets/**, then any other file in code-unit order, capped at 64
entries (the SKILL.md entry always keeps its slot). Dotfiles and build
directories (node_modules, .git, dist) are skipped; for a root-SKILL.md
package the reserved contribution folders and package-meta files are excluded
too.
There is no separate dir field: the bundle directory is always
dirname(path) — '.' for a root-SKILL.md package whose root is the skill.
The enumeration is display metadata (UI folder rows, the per-agent package
overview), never a permission boundary.
File-based entries override inline entries
A manifest may still declare tools, files, commands, hooks, or team members
inline in the neuralis block. Inline entries are a legacy fallback:
discovered file-based entries win by id (by name for tools, by slug for
team members), and inline entries without a matching file pass through
unchanged. Prefer files for everything — the inline form exists only so
imported manifests keep working.
Trust-tier sanitization at discovery
Discovery applies the package's trust tier to what it found. For any package
that is not first-party:
rules/contributions are downgraded todocs— a non-first-party package cannot inject always-on behavioral constraints into the system prompt.- A frontmatter
overwrite: trueflag is stripped. - An
activation.target: 'prompt-context'declaration is retargeted toworkspace.
This keeps the system-prompt injection surface a first-party privilege while still loading the contribution as readable reference material. See Features and access for the caller-side visibility rules that apply on top.