@package-system

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.wasm

Only 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

PathBecomesNotes
tools/*.jsonPackageTool[]Tool name = filename without .json. Invalid schemas fail the package load.
workflows/*.jsonPackageWorkflow[]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.mdskills array (category: 'skill')Folder-per-skill; the walk is recursive under skills/.
instructions/**/*.mdinstructions array (category: 'instruction')Arbitrarily named, recursive.
rules/**/*.mdrules array (category: 'rule')Arbitrarily named, recursive.
agents/**/*.mdagents array (category: 'agent')Delegate personas.
docs/**/*.mddocs array (category: 'docs')Reference material.
commands/*.jsonPackageCommand[]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.jsonPackageHook[]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.jsonTeamMemberContribution[]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/*.tsRoute handlersAuthored the same for node builtins AND WASM project packages; WASM builds emit dist/routes.json for the host feature-gate. See Routes.
src/tools/*.tsTool handlersDefault-export handler, paired with tools/<name>.json by filename.
src/channels/*.tsFirst-party realtime channelsExport matching channel, feature, and subscribe; compiled modules feed the shared events hub.
src/events/*.tsFirst-party event sourcesExport matching subject, a required visible, and an optional bridge; see Manifest → events.
src/runtimeProvider.tsRuntime providerA first-party provides: ["runtime"] package exports the named runtimeProvider.
src/lifecycle.tsLifecycle moduleSee 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 of skills/. 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 no package.json at all: the package root is the skill, and ${SKILL_DIR} resolves to it.

What is NOT scanned

  • Package-root loose .md files. 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 root SKILL.md.
  • Anything outside the category folders. There is no generic files/ contribution directory and no cross-folder contribution nesting — a rule must live under rules/, an instruction under instructions/, 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 to docs — a non-first-party package cannot inject always-on behavioral constraints into the system prompt.
  • A frontmatter overwrite: true flag is stripped.
  • An activation.target: 'prompt-context' declaration is retargeted to workspace.

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.

On this page