For AI agents: the complete documentation index is available at https://scriptedalchemy.github.io/agent-bundle/llms.txt, the full documentation bundle is available at https://scriptedalchemy.github.io/agent-bundle/llms-full.txt, and this page is available as Markdown at https://scriptedalchemy.github.io/agent-bundle/guide/start/project-structure.md.
  • English
  • Project structure

    An agent-bundle project is an ordinary Node package with one extra file at the root and a conventional src/ tree. Nothing here is mandatory: conventions fill the config in when it is silent, and config always wins when both describe the same thing.

    The layout

    my-plugin/
    ├── agent-bundle.config.ts     # project identity, targets, and policy
    ├── package.json               # authoritative release version and package identity
    ├── assets/                    # static files copied byte-for-byte into every artifact
    └── src/
        ├── skills/<name>/SKILL.md # one Skill per directory, with its own resources
        ├── commands/*.md          # host slash-command documents
        ├── rules/*.mdc            # host rule documents
        ├── hooks/*.ts             # lifecycle hook handlers referenced from config
        ├── mcp/<server-id>.ts     # a handwritten stdio MCP server entry
        ├── mcp/<server>/          # or a generated server, one module per route
        │   ├── tools/*.tsx
        │   ├── resources/*.tsx
        │   ├── prompts/*.tsx
        │   ├── apps/*.tsx         # browser MCP Apps compiled to self-contained HTML
        │   └── layout.tsx         # optional per-server layout around this server's routes
        ├── scripts/<name>.ts      # artifact scripts (.tsx renders through the Agent renderer)
        ├── cli.ts                 # a single package bin
        ├── cli/**/*.ts            # or a routed CLI, where nesting is the command path
        ├── index.ts               # the library entry
        ├── layout.tsx             # optional shared layout around every rendered route
        ├── state.ts               # project state definition
        └── providers/<name>.ts    # request-context providers

    What each root means

    PathSurfaceOpt out
    src/skills/<name>/SKILL.mdA Skill. Everything else in the directory ships as its resources. Ships with no declaration at all.Remove the directory, or narrow the skills config globs.
    src/commands/*.mdFlat host command documents. Frontmatter is judged per host: Claude Code documents description, argument-hint, allowed-tools, model, and disable-model-invocation, while Cursor's pinned commands surface is frontmatter-free Markdown. A command that explicitly targets a host which cannot express a field it uses is AB4927; an implicitly selected host receives the body minus the field and validate warns AB4928. inspect lists the same omissions as omittedFeatures.Remove the file.
    src/rules/*.mdcFlat host rule documents, emitted by Cursor, which keeps description, globs, and alwaysApply. The same per-host judgment applies: AB4907 for an explicit target, AB4908 as a warning for an implicit one.Remove the file.
    src/mcp/<server-id>.tsStdio entry for a declared MCP server that names no entry, command, or url.Declare entry explicitly.
    src/mcp/<server>/{tools,resources,prompts}/*Generated MCP server routes. The path supplies identity; each module supplies static config, schemas, and one async default Server Component.Set routes.servers.<server> to custom, command, or remote.
    src/mcp/<server>/apps/*Browser MCP App entries compiled to self-contained HTML and registered on the generated server. Static config.resourceUri is required.Use a custom server, or prefix the file with _.
    src/scripts/<name>.tsA plain script compiled to scripts/<name>.mjs in every selected target. Nested modules are a hard error (AB4808).Prefix a path segment with _, or claim the file with an explicit scripts entry.
    src/scripts/<name>.tsxA rendered script: the async default component receives argv and signal and renders through the Agent renderer with the CLI output contract.Rename to .ts, prefix a path segment with _, or claim the file.
    src/cli.tsA package bin named after plugin.name.bin: false
    src/cli/**/*.{ts,tsx}Routed CLI commands compiled into one collision-checked command graph and one executable. Nesting is identity: src/cli/library/audit.ts runs as <bin> library audit. Supersedes the src/cli.ts convention.bin: false, routes.cli: 'conventional', or prefix a path segment with _.
    src/index.tsThe library output, with declarations.lib: false
    src/layout.{ts,tsx}Shared document layout: default-exports one component receiving { children, route, signal } that renders Agent.Result around every rendered route — generated MCP tools, resources, and prompts, rendered routed-CLI commands, projected MCP commands, and rendered scripts. Event routes and browser Apps are never wrapped.Rename to _layout.tsx.
    src/mcp/<server>/layout.{ts,tsx}Per-server layout nested inside the root layout for that generated server's routes.Rename to _layout.tsx, or set routes.servers.<server> to a non-generated mode.
    src/state.tsProject state: default-exports defineState. Generated MCP, routed-CLI, and rendered-script request scopes mount it.state: false, or rename to _state.ts.
    src/providers/<name>.{ts,tsx}A request-context provider mounted at providers.<camelCaseName> on the request handle.Prefix the file with _.
    assets/Static resources copied byte-for-byte into every target artifact's assets/ directory.Declare a top-level assets list instead.

    Route and package entry conventions match .ts and .tsx files exactly; the state convention is specifically src/state.ts. Discovered entries carry provenance.kind: 'conventional' in the normalized model, so agent-bundle inspect tells you whether a file was picked up by convention or claimed by config.

    Config versus conventions

    The config holds what no single file can own — project identity, target selection, and policy:

    import { 
    const defineConfig: (config: AgentBundleConfig | ConfigFactory) => AgentBundleConfig | ConfigFactory
    defineConfig
    } from 'agent-bundle/config';
    export default
    function defineConfig(config: AgentBundleConfig | ConfigFactory): AgentBundleConfig | ConfigFactory
    defineConfig
    ({
    AgentBundleConfig.plugin: AgentBundlePluginConfig
    plugin
    : {
    AgentBundlePluginConfig.description?: string | undefined
    description
    : 'Evidence-backed project tools.',
    AgentBundlePluginConfig.name: string
    name
    : 'my-plugin' },
    AgentBundleConfig.targets?: string[] | undefined
    targets
    : ['portable', 'codex', 'claude'],
    });

    Add an explicit declaration only when you need something the convention cannot express — a different path, a target restriction, or an opt-out:

    import { 
    const defineConfig: (config: AgentBundleConfig | ConfigFactory) => AgentBundleConfig | ConfigFactory
    defineConfig
    } from 'agent-bundle/config';
    export default
    function defineConfig(config: AgentBundleConfig | ConfigFactory): AgentBundleConfig | ConfigFactory
    defineConfig
    ({
    AgentBundleConfig.plugin: AgentBundlePluginConfig
    plugin
    : {
    AgentBundlePluginConfig.description?: string | undefined
    description
    : 'Evidence-backed project tools.',
    AgentBundlePluginConfig.name: string
    name
    : 'my-plugin' },
    AgentBundleConfig.scripts?: Readonly<Record<string, AgentBundleScriptInput>> | undefined
    scripts
    : {
    // Restricted to one target, so it cannot ride the convention. 'detect-risk': {
    AgentBundleScriptEntry.entry: string
    entry
    : './src/scripts/detect-risk.ts',
    AgentBundleScriptEntry.targets?: readonly string[] | undefined
    targets
    : ['portable'] },
    },
    AgentBundleConfig.targets?: string[] | undefined
    targets
    : ['portable', 'codex', 'claude'],
    });

    Source validation reports informational nudges — never errors — when a project shows a pre-convention pattern: AB4730 for a self-connecting stdio entry that a default-exported factory would upgrade to the framework lifecycle shell, and AB4731 / AB4732 / AB4733 when src/cli.ts, src/index.ts, or src/mcp/<server-id>.ts exists but explicit configuration shadows it. The bin: false and lib: false opt-outs stay silent.

    Where output lands

    agent-bundle build writes two independent things.

    Host artifacts

    One directory per selected target under the artifact root. The CLI defaults that root to artifact/, so it never collides with the package build below; output.distPath or --output moves it:

    artifact/
    ├── agent-bundle.manifest.json # every emitted file with its SHA-256
    └── plugin/                    # targets: ['plugin'] — one multi-host bundle
        ├── .claude-plugin/
        ├── .codex-plugin/
        ├── .cursor-plugin/
        ├── bin/<plugin-name>.mjs      # the routed CLI, when src/cli/** exists
        ├── skills/
        ├── hooks/
        ├── mcp/
        ├── scripts/
        ├── assets/
        ├── AGENTS.md
        └── INSTALL.md

    Per-host layouts are available as the claude, codex, cursor, and portable targets. agent-bundle.manifest.json sits at the artifact root beside the target directories and records every emitted file with its SHA-256, so artifact validation is content-addressed rather than a guess.

    output.distPath moves the artifact root; it never changes the framework-owned layout inside each target. Precedence is the CLI --output, then output.distPath, then the default — artifact for agent-bundle build, which also emits the package build, and dist for the programmatic build() without packageOutputs. Values must be non-empty, project-root-contained relative POSIX paths.

    The npm package build

    When the project declares bin/lib — or provides them by convention — the same build also writes the node-consumable package build under dist/:

    dist/
    ├── bin/<name>.js              # self-executing ESM, shebang, executable bit
    ├── <stem>.js                  # the library entry
    └── **/*.d.ts                  # declarations, when lib.dts is on

    dist is a mandatory-ignored directory: package outputs never enter project source snapshots or Skill and asset discovery. The two outputs must not overlap: pointing output.distPath or --output at dist on a project with package entries is AB4706. The default already keeps them apart, and spelling it out is harmless:

    import { 
    const defineConfig: (config: AgentBundleConfig | ConfigFactory) => AgentBundleConfig | ConfigFactory
    defineConfig
    } from 'agent-bundle/config';
    export default
    function defineConfig(config: AgentBundleConfig | ConfigFactory): AgentBundleConfig | ConfigFactory
    defineConfig
    ({
    AgentBundleConfig.output?: AgentBundleOutputConfig | undefined
    output
    : {
    AgentBundleOutputConfig.distPath?: string | undefined

    The artifact output directory of agent-bundle build, relative to the project root. Defaults to dist. The per-invocation CLI --output flag still wins, but remains subject to the same project-root containment check; absolute and external output paths are unsupported.

    distPath
    : 'artifact' },
    AgentBundleConfig.plugin: AgentBundlePluginConfig
    plugin
    : {
    AgentBundlePluginConfig.description?: string | undefined
    description
    : 'A CLI plus a plugin.',
    AgentBundlePluginConfig.name: string
    name
    : 'my-plugin' },
    AgentBundleConfig.targets?: string[] | undefined
    targets
    : ['portable', 'claude'],
    });

    Next steps