Spec: Project DSL

Purpose

This spec defines the project DSL: the YAML file an operator authors to declare a kaged project. It is the source of truth for the agent tree (which agents exist and their parent-child dispatch relationships), what each agent can touch (cage and tools), which prompts they use, and which model backs each agent.

This document is normative for:

  • The schema (JSON Schema published at kaged.dev/schema/v1.json).
  • The parser's behavior — strict mode, error messages, cross-reference validation.
  • The CLI surfaces (kaged dsl validate, kaged dsl migrate).
  • What the daemon loads at session-start time.

It is not normative for:

  • The wire shape of API requests that operate on projects after they're loaded (that's http-api.md).
  • The runtime semantics of how the primary actually dispatches to subagents (that's daemon.md).
  • The cage enforcement mechanism — only the cage contract. The sandbox spec (sandbox.md) implements the contract this spec declares.

Constraints (from ADRs)

Constraint Source
Format is YAML 1.2 ADR-0006
Non-executable; never eval'd, never imported as code ADR-0006
Validated against published JSON Schema; mirrored as Zod internally ADR-0006
Strict mode: unknown fields are errors ADR-0006
Single file at .kaged/project.yaml (no imports, no includes in v1) ADR-0006
Schema versioned via top-level version: integer ADR-0006
Cage block is the operator-readable contract; enforcement is bwrap-shaped ADR-0009
Network allowlist is declarative; supports glob hostnames ADR-0009

File layout

<project root>/
├── .kaged/
│   └── project.yaml          # this file
└── prompts/
    ├── primary.md
    ├── scraper.md
    └── deployer.md           # referenced from project.yaml
  • Exactly one .kaged/project.yaml per project root.
  • Filename: project.yaml (not .yml).
  • Encoding: UTF-8, LF line endings, no BOM.
  • Path references (e.g., system_prompt: project:/prompts/primary.md) use a URI prefix (project:/ or config:/) per ADR-0015. Naked paths are rejected at parse time.

The shape, top-level

# yaml-language-server: $schema=https://kaged.dev/schema/v1.json
version: 1                       # required, integer
project: <slug>                  # required, string
description: <string>            # optional

primary:                         # required, AgentSpec (the root agent)
  model: <model-id>
  system_prompt: <path>
  cage: disabled                 # required; only `disabled` accepted for root agent (interim)
  tools:                         # optional, per-agent tool overrides
    "file.read": { enabled: true }
  parameters: <object>           # optional, model-specific
  description: <string>          # optional
  subagents:                     # optional, recursive named-object map
    <name>:                      # AgentSpec form (no `path:` field)
      model: <model-id>
      system_prompt: <path>
      cage: <cage-block> | disabled
      tools: <tools-map>         # optional, per-agent tool overrides
      parameters: <object>       # optional
      description: <string>      # optional
      subagents:                 # optional, recursive (depth limit 16)
        <name>: ...
    <name>:                      # Project-reference form (presence of `path:` is the discriminator)
      path: project:/sub/path    # required; project:/ only in v1
      name: <string>             # optional; tool-name override presented to the LLM
      description: <string>      # optional; description override presented to the LLM
      overrides: <object>        # optional; partial ProjectDsl deep-merged on top of nested project

plugins:                         # optional, named-object map (project-level registry)
  <plugin-id>:
    package: <string>            # required; plugin package identifier
    source: <string>             # optional; install source with magic prefix
    enabled: <boolean>           # optional; project-wide default (default: false)
    config: <object>             # optional; plugin-specific project defaults

tasks:                           # optional, named-object map
  <name>:
    command: <string>
    description: <string>        # optional
    group: <string>              # optional
    cwd: <path>                  # optional
    long_running: false          # optional
    confirm: false               # optional
    env: <object>                # optional

The full JSON Schema is in Appendix A. Field-by-field semantics are below.


Fields

version (integer, required)

The schema major version. Currently 1.

  • Required. Must be the first non-comment key.
  • Integer, not string. version: "1" is invalid.
  • Major bumps only. Minor amendments to the schema do not bump this; they're additive.
  • Forward-compat: A kaged version that supports schema v1 will error on a file declaring version: 2. It will not "try its best."
  • Migration: kaged dsl migrate <file> rewrites a v1 file to the current major if a migration path exists; otherwise errors with a diff.

project (string, required)

The project's slug. Identifies the project across logs, the audit trail, the URL of the web UI, and the storage namespace.

  • Required.
  • Pattern: ^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$ — lowercase letters, digits, and hyphens; 2-64 chars; cannot start or end with a hyphen.
  • Unique per kaged installation. If two project directories declare the same slug, the daemon errors at session-start time and refuses to load either.

description (string, optional)

Human-readable description of the project. Used in the web UI as the project subtitle. Plain text; no markdown rendering.

  • Optional. Default: empty string.
  • Max length: 280 characters (one tweet's worth; if you need more, write a README).

primary (AgentSpec, required)

The root agent for the project. Exactly one. This is an AgentSpec — the same recursive shape used at every position in the agent tree per ADR-0022. See § AgentSpec below for the full shape.

The root agent has two special properties that non-root agents do not:

  1. Role-based default tools. The root agent gets all kaged.* tools enabled by default: kaged.checkpoint, kaged.issue.*, kaged.ask, and kaged.form. All other namespaces (file, edit, search, code, debug) start disabled and must be explicitly opted in via operator config ([default_tools]) or the project's primary.tools block. Every other agent in the tree starts with an empty tool set; the operator opts in per agent.
  2. Cage interim restriction. Until the supervisor supports caging the primary process, the only legal value for the root agent's cage is disabled. The parser emits a parse-time error for any other value. Non-root agents accept the full cage block.

AgentSpec

AgentSpec is the single recursive shape for every agent in the project tree. It is used identically for ProjectDsl.primary (the root agent) and for every entry under any subagents map at any depth. There is no PrimaryAgent vs Subagent distinction — the only differentiation is positional (root vs non-root).

Per ADR-0022: the tree structure is the call graph. A parent agent can call its direct children; sibling and cross-tree calls do not exist.

# AgentSpec — used at every agent position
model: <model-alias>             # required
system_prompt: <path>            # required
cage: <cage-block> | disabled    # required
description: <string>           # optional
parameters: <object>            # optional
include_tool_results_in_context: <boolean>  # optional, default true
tools:                           # optional, per-agent tool overrides
  "<tool-name>":
    enabled: true
    description: <string>
    parameters: <object>
  "<glob>": null                 # nullify = reset to defaults
plugins:                         # optional, per-agent plugin overrides
  <name>:                        # must reference a slot in the project-level registry
    enabled: <boolean>           # optional; override registry default
    hooks: [ <hook-name>, ... ]  # optional, lifecycle hooks to subscribe to
    config: <object>             # optional, override registry config
compaction:                      # optional, per-agent compaction (ADR-0024)
  strategy: drop | summarize | delegate | checkpoint  # optional, default 'drop'
  upper_threshold: <0.0..1.0>    # optional, default 0.85
  lower_threshold: <0.0..1.0>    # optional, default 0.60
  always_keep: [ <predicate>... ] # optional
  summarize: <object>            # optional, defaults provided (prompt, window, preserve_recent)
  delegate: <object>             # optional, strategy-specific
subagents:                       # optional, recursive named-object map
  <name>: <AgentSpec>            # same shape, recursively
  <name>: <ProjectReference>    # or a project reference (presence of `path:`)
  <name>: null                  # ADR-0015 nullification

AgentSpec fields

model (string, required)

A model alias — a free-form name that the operator's local config resolves to a concrete provider:model identifier (per ADR-0011).

  • Examples: smart-generalist, low-cost-coder, local-only, my-codereviewer.
  • Pattern: ^[a-z][a-z0-9-]{0,62}[a-z0-9]$ — lowercase letters, digits, hyphens; 2-64 chars.
  • Must NOT contain a colon. A value containing : (e.g. claude:sonnet-4.6) is rejected at parse time with a clear error pointing the operator to ADR-0011. The DSL is portable; concrete provider:model strings live only in local-config.md.
  • Reserved names (not usable as aliases): primary, subagent, operator, system, default.
  • Resolution timing: parse-time validation checks the shape of the alias name only. Whether the operator's local config has a binding is checked at project-load time (see Validation timing) — an unresolved alias keeps the project in pending state until the operator binds it.
  • Provider reachability (does the resolved provider's base_url respond? does the API key work?) is checked at session-start time — same as before. A bound alias whose provider is unreachable produces a 502 provider_unreachable per http-api.md.

Recommended starter aliases (kaged ships with these as suggestions, not enforcement):

Alias Intended role
smart-generalist Default primary; capable general-purpose model
smart-careful Higher-quality (slower, more expensive) model for hard work
low-cost-fast Cheap fast model for simple tasks
low-cost-coder Cheap fast model with reasonable coding ability
local-only Whatever local model the operator runs
tiny Minimal local model for trivial tasks

Projects are encouraged to use these where they fit and document custom aliases in their README. See local-config.md for the binding mechanism.

system_prompt (string, required)

A path to a markdown file containing the agent's system prompt. Uses a URI prefix (project:/ or config:/) per ADR-0015.

  • Required.
  • Path rules:
    • Must use a URI prefix. Naked paths are rejected at parse time.
    • Must NOT escape the project root via .. segments.
  • Validation at parse time: the parser checks the path's shape (prefix, no .. escape). File existence is checked at project-load time (so a project being authored can validate without prompt files yet existing); a missing prompt at session-start fails the session, not the parse.
  • Format: Markdown, optionally with YAML frontmatter. Frontmatter is opaque to the DSL parser; the daemon's prompt loader handles it. See prompts spec (not yet written).

cage (object or string, required)

The cage policy. See cage block below.

Two forms are accepted:

  1. Object form — a full cage block declaring fs, net, state, optional seccomp and limits. This is the default and expected shape.
  2. String form — the literal value disabled.
# object form (the default)
cage:
  fs: []
  net:
    allow: []
  state: ephemeral

# string form — opt out of sandboxing entirely
cage: disabled

Rules:

  • Required field on every agent. You must write cage: even when you want it disabled. There is no implicit cage — silence is never the policy. There are no project-level cage defaults; each agent declares its own cage independently.
  • Root agent interim restriction. Until the supervisor supports caging the primary process, the only legal value for the root agent's cage is disabled. The parser emits a parse-time error for any other value on the root agent. Non-root agents accept both forms.
  • No "wide-open" shortcut for the object form. To grant broad access via a real cage, the operator must write it out (fs: [{mode: rw, path: /}] etc.), which is intentionally ugly. Operators who want full host access should use cage: disabled instead — it's honest about what's happening.
  • cage: disabled semantics (per ADR-0009 amendment):
    • The supervisor spawns the agent as the daemon's UID with no bwrap wrapper, no namespacing, no seccomp filter, no cgroup limits beyond the daemon's own.
    • The agent has full host filesystem read-write access (whatever the daemon user has).
    • The agent can reach any network destination the daemon user can reach.
    • The agent runs in the daemon's process tree (it can be signaled by other processes the daemon user owns).
    • The honest framing: an uncaged agent IS your daemon's hands. Same UID, same access, same blast radius.
  • No cage inheritance. Each agent declares its own cage; there is no inheritance between parent and child cages because cages are per-process and a child runs in its own sandbox context. The removed cage_defaults is not replaced by any implicit defaulting mechanism.
  • Parse-time warning. The DSL validator emits a warning (not an error) for every cage: disabled entry on a non-root agent. The warning text names the agent and links to ADR-0009. (The root agent's cage: disabled is the only legal value in the interim — no warning.)
  • Runtime visibility. The web UI shows uncaged agents with a magenta [UNCAGED] badge instead of the standard [CAGED] badge. Every spawn of an uncaged agent emits an agent.spawn.uncaged audit event.
  • Daemon-wide override: when the daemon runs with --no-sandbox (ADR-0009), every agent's cage is treated as disabled regardless of what its DSL declares. The DSL is still parsed and validated (so the file remains portable to a sandboxed daemon) but the cage block is not enforced.

description (string, optional)

Human-readable description of the agent. Used in the UI and as the tool description when this agent is presented to its parent as a synthetic agent-<key> tool.

  • Optional. Default: empty string.
  • Max length: 280 characters.

parameters (object, optional)

Model-specific parameters (temperature, max_tokens, top_p, etc.). Opaque to the DSL parser. The daemon passes them through to the model provider.

  • Optional. Default: provider defaults.
  • Schema: any object. The daemon validates against the specific provider's accepted parameters at session-start time.

max_steps (integer, optional)

Maximum number of tool-call rounds the agent loop is allowed before stopping. Passed to the Mastra Agent.stream() execution options.

  • Optional. Default: provider/model defaults (Mastra v1.36.0 default is 5; kaged recommends 20 for complex tasks).
  • Schema: integer, 1100. Values outside this range are rejected at parse time.
  • Per-agent. Each agent in the tree can set its own limit. Subagents inherit from parent unless explicitly overridden.

max_output_tokens (integer, optional)

Maximum number of output tokens the model may generate in a single completion. This is the output token budget, not the context window. Passed to the provider as max_tokens (or provider-specific equivalent).

  • Optional. Default: provider defaults (Fireworks default for kimi-k2p6-turbo is ~1024; kaged recommends 4096+ for tool-heavy agents).
  • Schema: integer, 165536. Values outside this range are rejected at parse time.
  • Per-agent. Each agent can set its own budget. A thinking model (e.g., kimi) may consume thinking tokens from this budget before emitting visible text.
  • Note: This is a request to the provider. The provider may stop earlier (finish reason stop) or hit this ceiling (finish reason length). The UI surfaces the actual stop reason so the operator can adjust.

tools (object, optional)

Per-agent tool configuration overrides. Keys are tool names (dot-delimited, e.g. file.read, code.lsp) or glob patterns (debug, *). Values are either a ToolOverride object or null (ADR-0015 nullification).

This field replaces the former project-level tools: block. Each agent declares its own tool surface.

tools:
  "file.read":
    enabled: true
    description: "Read file contents (project-scoped)"
  "debug":
    enabled: false                # disable this tool for this agent
  "search.grep":
    parameters:
      max_results: 500            # merge into default parameters
  "code.lsp": null                # nullify — reset any inherited overrides
  • Optional. If absent, tool defaults apply based on the agent's position (see Tool resolution below).
  • Keys are dot-delimited tool names matching the tool registry's naming convention (<namespace>.<tool>). See agent-tooling.md for the full built-in catalog.
  • Values are either:
    1. ToolOverride object — with any subset of: enabled (boolean), description (string), parameters (object).
    2. null — ADR-0015 nullification. Resets any overrides, restoring the tool to its built-in defaults.
  • kaged.issue.* and kaged.workflow.* tools carry a principal_scope: "root-only" tag. The schema rejects these tools on non-root agents. Attempting to enable kaged.issue.create on a subagent is a parse error.
Tool resolution

Tool resolution applies to every agent in the tree, not just the root. The layered order is the same at every level (later wins):

  1. Available tools — the set of tools registered in the runtime's ToolRegistry (all 19 built-in tools across 9 namespaces: file, edit, search, code, debug, shell, compute, project, kaged). Only tools actually registered can appear in the resolved set.
  2. Default-enabled base state — the root agent starts with kaged.* tools enabled (DEFAULT_ENABLED_TOOLS); all non-root agents start with nothing enabled. This is the key difference: subagents must explicitly opt in to every tool via their own tools: block or operator config.
  3. Operator-level overrides — from local.toml [default_tools] (see local-config.md). Applied system-wide to all agents. Use { enabled: true } to opt in a disabled-by-default tool, or null / { enabled: false } to disable a default-enabled tool.
  4. Agent-level overrides — the agent's own tools: block (this field). Per-agent policy; overrides operator defaults.
  5. Cage filter at dispatch — even if a tool is enabled, dispatch() still checks cage permissions before execution. A caged agent calling file.write on a path outside its cage.fs mounts gets capability_denied.

When availableTools is provided to compileProjectDsl(), the compiler materializes a tools: { <name>: { enabled: true/false } } block on every agent in the tree — root, inline subagents, and project-reference primaries. This materialized view is the "total render" visible in the synthesized endpoint.

Resolution is performed by resolveRootTools() in @kaged/dsl (exported from packages/dsl/src/defaults.ts). The function accepts the available tool names, an ordered list of override layers, and a defaultEnabled set (root uses DEFAULT_ENABLED_TOOLS, subagents use an empty set). It returns the filtered list of enabled tool names.

The compileProjectDsl() function accepts optional availableTools and operatorToolOverrides in its options. When provided, the compiled result includes resolvedRootTools: string[] — the effective root-agent tool list. When omitted, resolvedRootTools is null. The synthesized endpoint passes DEFAULT_ROOT_TOOLS as availableTools and the operator's default_tools as operatorToolOverrides, so resolved_tools is always populated in its response.

Future: plugins will inject additional tools into the available set before compilation. The availableTools parameter accommodates this — the daemon pre-merges plugin-provided tools into the registry before passing names to the compiler.

This separation is intentional: tools: is the operator's per-agent policy; cage: is the operator's security boundary.

include_tool_results_in_context (boolean, optional, default true)

Controls whether prior tool calls and their results are included in the LLM conversation context when reconstructing message history for subsequent turns.

  • Default: true. Tool calls (with arguments) and tool results (with output) from prior turns are included in the message history sent to the LLM, giving the model full visibility into what tools were called and what they returned.
  • When false: Only the text content of prior assistant messages is included. Tool calls and results are stripped from the context. This reduces token usage at the cost of the model losing awareness of prior tool interactions.
  • Per-agent. Each agent in the tree can set this independently. A parent agent might include tool results while a cheap subagent omits them to save tokens.
  • Future: This field is the natural hook for context compaction strategies — a future processor could selectively summarize or truncate tool results based on age, size, or relevance.

plugins (named-object map, optional)

The project-level plugin registry. Every plugin used in the project MUST be declared here first. This is the gateway — it makes the plugin available to agents, system services, and the UI. Per-agent plugins: blocks only override or disable; they can never introduce a plugin the registry doesn't know about.

plugins:
  memory:
    package: "@kaged/memory-markdown"
    source: "npm:@kaged/memory-markdown"
    enabled: true
    config:
      isolation: project
      max_entries: 1000

  audit:
    package: "@example/audit-log"
    source: "github:example/audit-log"
    enabled: false
    config:
      log_level: info
  • Keys are operator-chosen plugin slot names (slug format ^[a-z][a-z0-9_-]*$). The slot name is independent of the package name — a project may have multiple plugins of the same package under different slots, though this is rare. Per ADR-0015, setting a key to null in project.local.yaml removes the plugin (operator opt-out).

  • package (string, required) — the plugin package identifier (e.g. @kaged/memory-markdown). Resolved at project-load time against the operator's local plugin store; missing plugins trigger the install prompt per plugin-host.md § Install flow.

  • source (string, optional) — where to fetch the plugin if it's not installed. Uses magic prefix conventions:

    • npm:<package> — install from npm registry
    • github:<owner>/<repo> — clone from GitHub
    • project:/<path> — relative to the project root
    • config:/<path> — relative to the operator's config directory
    • git:<full-url> — clone from any Git URL (SSH or HTTPS)

    If source is omitted and the plugin isn't installed, the daemon reports it as missing but cannot offer to install it.

  • enabled (boolean, optional, default false) — the project-wide default. When true, the plugin is active for agents that don't override. When false, agents must explicitly set enabled: true to use it.

  • config (object, optional) — the plugin's project-side configuration. Opaque to the DSL parser; validated by the plugin's config_schema from its manifest at project-load time. Project-committed; never contains secrets (per ADR-0023 project/system config split).

Per-agent plugin overrides

Agents reference plugins by their registry slot name. The per-agent block overrides enabled, hooks, and config:

primary:
  plugins:
    memory:
      enabled: true
      hooks: [ on_session_start, on_session_idle ]
      config:
        store: "config:/memory"
    audit:
      enabled: false               # opted out

  subagents:
    researcher:
      plugins:
        memory:
          enabled: true
          hooks:
            - pre_compact
            - post_compact
          config:
            isolation: agent       # override: this agent gets its own memory
  • Per-agent slot names must reference a key in the project-level registry (cross-reference validation error if not).

  • enabled (boolean, optional) — overrides the registry default for this agent.

  • hooks (list of strings, optional), which are lifecycle hooks this plugin subscribes to on this agent. Allowed values: on_session_start, on_session_idle, pre_compact, post_compact. See plugin-host.md § Lifecycle hooks.

    • on_session_start and on_session_idle only fire on the primary (per ADR-0023 — sessions are primary-owned). Declaring these on a subagent is permitted in the DSL but produces a warning at load time and the hook never fires.
    • pre_compact and post_compact fire per-agent.
  • config (object, optional) — deep-merged on top of the registry config. Allows per-agent config overrides without repeating the entire config block.

  • Tool registration. Tools declared in the plugin's manifest are auto-enabled on every agent that enables the plugin. The agent's tools: block can disable specific plugin tools as an escape hatch (per plugin-host.md § Plugin tool naming):

primary:
  plugins:
    memory:
      enabled: true
  tools:
    "memory-markdown.recall": { enabled: false }  # last-resort disable
  • Max entries: 16 plugins per agent.

compaction (object, optional)

Per-agent context compaction configuration. Per ADR-0024, each agent has its own context window and its own compaction events. Subagents inherit defaults from their parent if unset; the parser materializes the resolved per-agent compaction at compile time so the synthesized DSL endpoint shows the effective value.

primary:
  compaction:
    strategy: summarize
    upper_threshold: 0.85
    lower_threshold: 0.60
    summarize:
      model: cheap-summarizer
      window_messages: 20
      preserve_recent: 10
      prompt: project:/prompts/compactor.md
    always_keep: []
  • Optional. If absent on an agent, the agent inherits its parent's compaction config. If the entire tree has no compaction: declared, kaged falls back to:

    strategy: drop
    upper_threshold: 0.85
    lower_threshold: 0.60
    always_keep: []
    summarize:
      prompt: config:/prompts/compaction-summary.md
      window_messages: 20
      preserve_recent: 10
      max_summary_tokens: 1500
    

    The default includes a summarize block so the drop strategy can attempt summarization before falling back to pure drop (see § Default compaction summary prompt).

  • strategy (enum, default 'drop') — one of 'drop', 'summarize', 'delegate', 'checkpoint'. See agent.md § Compaction for full semantics of each.

  • upper_threshold (number, 0.0..1.0, default 0.85) — fraction of the model's context window at which compaction triggers. Crossing this fires the compaction pipeline.

  • lower_threshold (number, 0.0..1.0, default 0.60) — fraction the strategy compacts down to. Hysteresis: prevents oscillation when the message list is near the upper bound. Must be < upper_threshold (validation error if not).

  • always_keep (list of strings, optional) — operator-configured always-keep predicates. Each entry is a predicate identifier the harness recognizes (e.g. first_user_message, messages_with_tag:critical). The harness's defaults — system prompt and first operator message — are always included regardless. See agent.md § Always-keep set.

Strategy-specific fields

summarize (object, required when strategy: summarize):

summarize:
  model: <model-alias>             # optional — alias resolved per local-config; when omitted, uses the agent's own model
  window_messages: 20              # optional, default 20 — how many messages to compress per event
  preserve_recent: 10              # optional, default 10 — always keep N most recent intact
  prompt: config:/prompts/compaction-summary.md   # optional, default config:/prompts/compaction-summary.md — summarizer prompt
  max_summary_tokens: 1500         # optional, default 1500 — budget for the summary itself
  • model is optional. When omitted, the summarizer uses the agent's own model alias. Operators who want a cheaper model for summarization can set this explicitly.
  • prompt is optional with a default of config:/prompts/compaction-summary.md. This file is auto-created by kaged project init if it does not exist (see § Default compaction summary prompt). Operators can override it with a project:/ path for project-specific prompts.

The summarizer model is invoked via the standard provider router (same path as the primary). Cost is tracked separately in the session's stats (per agent.md § Cost surfacing).

delegate (object, required when strategy: delegate):

delegate:
  plugin: <plugin-slot-name>       # required — the name key from `plugins:` declaration above
  fallback_strategy: drop          # optional, default 'drop' — what to do if the plugin fails

The named plugin must be declared in this agent's plugins: block AND must claim role: compactor in its manifest. Parse-time validation enforces both.

checkpoint (object, optional when strategy: checkpoint):

checkpoint:
  fallback_strategy: drop          # optional, default 'drop' — what to apply if operator approves without editing
  auto_resume_timeout_sec: null    # optional, default null (wait indefinitely)

When the strategy is checkpoint, the session pauses; the operator inspects the proposed compaction in the Compactor UI (per ui/compactor.md) and approves, edits, or rejects.

Inheritance

Subagents inherit their parent's compaction: block. Override is field-level: any field specified on the subagent's compaction: overrides the same field; non-specified fields inherit:

primary:
  compaction:
    strategy: summarize
    summarize: { model: cheap-summarizer, ... }
  subagents:
    researcher:
      compaction:
        # inherits primary's summarize.{model, ...} and thresholds
        upper_threshold: 0.75       # researcher compacts earlier (cheap model with smaller window)
Default compaction summary prompt

Every kaged project ships a default compaction summary prompt at config:/prompts/compaction-summary.md. This is the prompt used when the summarize strategy (or the drop-with-summarize-fallback) invokes a model to compress older messages into a structured handoff.

Auto-creation. kaged project init scaffolds this file into .kaged/prompts/compaction-summary.md alongside the existing default.md system prompt. If the file is missing when a project is loaded (e.g. operator deleted it, or an older project predates this feature), the daemon writes the default content at project-load time. The operator is free to edit the file after creation — it is operator-owned config, not a managed artifact.

Default content. The default prompt produces a structured context checkpoint handoff with these sections:

  • Goal — user goals; list multiple if session covers different tasks.
  • Constraints & Preferences — constraints or requirements mentioned.
  • Progress — Done (completed tasks/changes), In Progress (current work), Blocked (issues preventing progress).
  • Key Decisions — decisions with brief rationale.
  • Next Steps — ordered list of next actions.
  • Critical Context — important data, pending questions, references.
  • Additional Notes — anything else important not covered above.

The prompt instructs the summarizer to preserve exact file paths, function names, error messages, and relevant tool outputs. If the conversation ends with an unanswered question or pending request, the prompt requires that exact question to be preserved in the summary.

Override. Operators can point compaction.summarize.prompt to any project:/ or config:/ path. The default is used only when no explicit prompt is set (or when it is set to the default config:/prompts/compaction-summary.md).

Dogfood. The kaged-on-kaged config (.kaged/prompts/compaction-summary.md in this repo) uses the same default prompt.

subagents (named-object map, optional)

A map of child agents this agent may dispatch, keyed by name. Order is not significant. Child agents are spawned on demand by their parent, not at session-start.

  • Optional. If absent or empty, the agent has no children — it is a leaf node.
  • Max entries: 64 per agent (per level).
  • Keys are agent names — uniqueness is structural (duplicate YAML keys are a parse error).
  • Override semantics: in project.local.yaml, setting a key to null removes that agent from the merged result (ADR-0015 nullification).
  • Recursive nesting. subagents may itself contain agents with their own subagents. Depth is bounded at 16 levels (same as the existing project-reference depth limit per ADR-0015). The tree is the call graph: a parent agent can call its direct children; sibling and cross-tree calls do not exist.
Agent key (string, required)

The agent's name within its parent's scope. Used in the UI, in the audit log, and as the synthetic tool name (agent-<key>) the parent's LLM sees.

  • Pattern: ^[a-z][a-z0-9_]{0,30}[a-z0-9]$ — lowercase letters, digits, and underscores; 2-32 chars; starts with a letter; cannot end with underscore.
  • Reserved names: primary, operator, system. Using these is a parse error.

Each value under subagents is either an AgentSpec (same recursive shape) or a ProjectReference (see Project-reference subagents).


Default cage profile

When an agent's cage object form does not specify optional fields (seccomp, limits), kaged applies:

seccomp: default
limits:
  memory_mb: 256
  cpu_shares: 1024
  pids: 64
  walltime_sec: 600

The required cage fields (fs, net, state) have no implicit defaults — each agent must declare them explicitly. Maximally restrictive starts with fs: [], net: { allow: [] }, state: ephemeral — the cage starts with nothing; every grant is explicit.


Project-reference subagents

A subagent value may be a project reference instead of a full subagent declaration. The parent project points at a nested kaged project, and the nested project's primary is exposed to the parent as a callable subagent.

This is the surface implementation of ADR-0015 §7 ("Compiled Contextualization") and §6 ("Cross-Project Injection"). It is how a parent project composes other projects as units of work, without coupling either project to the other's internals — each project remains a self-contained silo per ADR-0015 §1.

subagents:
  # AgentSpec form (inline agent)
  scraper:
    model: low-cost-fast
    system_prompt: project:/prompts/scraper.md
    cage:
      fs: []
      net: { allow: [example.com] }
      state: ephemeral
    tools:
      "search.grep": { enabled: true }

  # Project-reference form
  builder:
    path: project:/sub/frontend-builder
    name: ui_builder                  # optional: tool name presented to the LLM
    description: Builds the frontend  # optional: description presented to the LLM
    overrides:                        # optional: partial ProjectDsl deep-merged on top
      primary:
        model: smart-careful

Discriminator

The parser distinguishes the two forms by the presence of path:.

  • path: absent → value is an AgentSpec. Must satisfy the full AgentSpec schema (model, system_prompt, cage).
  • path: present → value is a ProjectReference. Must satisfy the project-reference schema below. model, system_prompt, cage, and tools are forbidden at this layer — they belong to the nested project's own DSL.

Mixing fields from both shapes (e.g. path: and cage: on the same entry) is a parse error.

subagents.<name>.path (string, required for project references)

URI-prefixed path to the nested project's root directory (the directory containing .kaged/project.yaml).

  • Required for the project-reference form. Its presence is what selects this shape.
  • Accepted prefixes in v1: project:/ only. The path must resolve to a directory inside the parent project's root, per ADR-0015 §1 ("project root is the ceiling"). config:/ is rejected (the nested project is project content, not configuration metadata).
  • Other prefixes (git:/, https:/, file:/) are explicitly deferred. They depend on ADR-0015 §4 ("security ceiling") which is currently unimplemented per federated-config.md. A later amendment lifts this restriction.
  • The resolved path must contain .kaged/project.yaml. Existence is checked at project-load time, not parse time. Parse time validates only the URI shape.
  • No .. escape, per the standard URI rules in federated-config.md.
  • A nested project cannot reference the parent or any ancestor. Cycle detection at compile time (see Compilation and cycles below).

subagents.<name>.name (string, optional)

Override the tool name the subagent is presented as to the LLM. Since every subagent surfaces to the model as a tool, this is purely a presentation hint.

  • Optional. Default: the nested project's project: slug.
  • Pattern: the same subagent-name pattern (^[a-z][a-z0-9_]{0,30}[a-z0-9]$). Reserved names (primary, operator, system) are rejected.
  • Does NOT change the map key. The map key (builder in the example above) remains the canonical local identifier — it is what audit logs record, what the operator reviews. name: only affects the string the LLM sees.
  • Rationale: the operator wants short, model-friendly tool names; the operator also wants project-meaningful map keys. These goals don't always agree. Splitting them keeps both honest.

subagents.<name>.description (string, optional)

Override the tool description presented to the LLM.

  • Optional. Default: the nested project's top-level description: field (or empty if absent).
  • Max length: 280 characters, matching the project-level description constraint.
  • Plain text. No markdown rendering.

subagents.<name>.overrides (object, optional)

A partial ProjectDsl deep-merged on top of the nested project's own resolved config, per ADR-0015 §2 merge semantics.

  • Optional. Default: no overrides; the nested project loads as-is.
  • Merge order (low → high precedence):
    1. Nested project's .kaged/project.yaml
    2. Nested project's .kaged/project.local.yaml (if present on the operator's machine)
    3. Parent's subagents.<name>.overrides block
  • Deep merge with nullification. Same algorithm as federated-config.md § Configuration merging. Setting a key to null removes it; objects deep-merge; arrays replace.
  • version and project are forbidden in overrides. Project identity cannot be re-stamped by the parent (same rule as project.local.yaml).
  • Schema validation runs on the merged result, not on the partial override. The override alone need not be a valid ProjectDsl; the merged output must.
  • No cage widening shortcut. If the parent wants to tighten a nested agent's cage, it does so explicitly via overrides.primary.cage or overrides.primary.subagents.<nested-name>.cage. Each agent in the nested project declares its own cage; there is no inheritance to manipulate. See Cage policy for nested projects below.
  • Path references inside overrides resolve against the nested project's root, not the parent's. system_prompt: project:/prompts/x.md in an override points at <nested-root>/prompts/x.md. Otherwise the override would break the nested project's portability.

Cage policy for nested projects

Nested-project agents keep their own cages, declared in the nested project's own DSL. Each agent declares its own cage independently; there is no inheritance between parent and child cages. The project-reference itself has no cage: field.

This preserves ADR-0011 portability (the nested project is self-contained and runnable in isolation) and is consistent with ADR-0015 §4's "security ceiling" direction (a parent should not be able to widen a child's policy; if the ceiling were enforced it would be expressed via overrides, not via implicit defaulting).

If the parent wants to tighten a nested agent's cage for this composition, it does so via overrides:

subagents:
  builder:
    path: project:/sub/frontend-builder
    overrides:
      primary:
        subagents:
          # The nested project declares `compiler` with broad net access;
          # the parent restricts it for this composition.
          compiler:
            cage:
              net:
                allow: []   # no network for this composition

A future amendment, once ADR-0015 §4 lands, will formalize the security ceiling so the parent's tightening is enforced even if a malicious overrides block tries to widen instead.

Cross-references and call graph

Project-reference agents participate in the parent agent's call graph by map key. The tree structure is the call graph per ADR-0022: a parent agent can call its direct children; sibling and cross-tree calls do not exist.

  • The project-reference has no can_be_called_by field — call capability is implicit from the tree position. The parent agent that declares the project-reference as a child can call it.
  • The project-reference's nested project's primary becomes this entry's AgentSpec, with the nested project's subagents becoming this entry's subagents, recursively. After flattening, the shape is uniform top-to-bottom.

Compilation and cycles

At project-load time, the daemon performs a compilation pass per ADR-0015 §7: it walks the project-reference tree, resolves each nested project, and produces a single in-memory manifest the runtime uses to dispatch.

Algorithm

The compiler is a recursive walk over the parent's subagents map. For each entry that satisfies isProjectReference, the compiler:

  1. Resolves the path. path: project:/sub/builder is resolved against the current project's root (not the topmost ancestor) per ADR-0015 §1 silo boundary. Result: an absolute filesystem path to the nested project's root directory.
  2. Reads the nested project. Looks for <resolved-root>/.kaged/project.yaml. If missing, compilation fails with a nested_project_missing diagnostic naming the offending map key and the resolved path.
  3. Loads the nested overlay. If <resolved-root>/.kaged/project.local.yaml exists, it is layered on top via the standard loadProjectDsl overlay merge.
  4. Applies the parent's overrides block. The reference's overrides (if any) is deep-merged on top of the nested project's already-resolved DSL using ADR-0015 §2 semantics (deep merge, null nullifies, version and project rejected). Schema validation runs on the merged result.
  5. Recurses. The compiler walks the nested project's own subagents map. Project-refs there are resolved against the nested project's root.

The compiled output is a uniform AgentSpec subtree: the nested project's primary becomes this entry's AgentSpec, with the nested project's subagents becoming this entry's subagents, recursively. The original ProjectReference metadata (path, name, description, overrides) is retained as a _source annotation alongside the compiled AgentSpec so downstream consumers can trace each entry back to its declaration. After compilation, the tree is AgentSpec top-to-bottom — there is no ProjectReference vs AgentSpec distinction in the compiled manifest.

Cycle detection
  • Arbitrary depth supported. Project A may reference B, which may reference C, and so on.
  • Cycle detection at compile time. The compiler tracks visited absolute root paths along the current walk. If a path is re-entered, compilation fails with a compile_cycle diagnostic naming the chain (e.g. A (/foo) → B (/foo/sub) → A (/foo)). The parent project enters invalid state.
  • Depth limit. Compilation aborts with a compile_depth_exceeded diagnostic when the walk exceeds 16 levels. Configurable for tooling that needs deeper introspection; the daemon ships with the default. The limit guards against pathological non-cyclic chains and bounds load time.
Cage policy is not rebased

Cage paths inside a nested project remain project-root-relative to the nested project's own root, exactly as authored. cage.fs[].path: data in nested project B resolves to <B-root>/data at runtime, not <parent-root>/.../data. The compiled tree carries the nested DSL verbatim; the runtime mount resolver applies the nested root.

This preserves the portability promise: a nested project that runs standalone uses the same paths it does when composed into a parent.

Failure semantics

When any step above fails, compilation surfaces a DslError carrying DslDiagnostic[] with one entry per failure. The diagnostic includes:

  • The offending map key path (e.g. primary.subagents.builder for a top-level ref, primary.subagents.builder.subagents.inner for a deeper failure).
  • The resolved filesystem path involved.
  • The underlying cause (missing file, parse error, schema-validation failure of the merged result, cycle path, depth limit, override violation).

The GET /api/v1/projects/:id/dsl/synthesized endpoint surfaces these as a 422 response per http-api.md. Project status at the daemon's registry transitions to invalid when the compilation step itself fails (as opposed to the file being missing on first load, which is pending).

Other compile-time properties
  • State is siloed per ADR-0015 §7. When the nested project runs as a subagent of the parent, the nested project's conversation logs and ephemeral state are not inherited; the nested project operates under the parent's session state. The nested project's own session state (when it is run standalone) is unaffected.
  • A nested project does not see its parent. The nested project's DSL has no field referring to the parent and no way to introspect it. This is the portability promise of ADR-0011.

What this is not

  • Not a hot-reload mechanism. Changing the nested project's DSL while the parent's session is running does not propagate until the parent session restarts (or a hot-reload story is specified in daemon.md).
  • Not a cross-machine reference. Nested projects in v1 live inside the parent project's root tree. Remote references (git:/, https:/) are deferred.
  • Not parent injection. ADR-0015 §6 describes a complementary mechanism where a parent injects virtual subagent definitions into a child. That is not what this section specs — that mechanism is still deferred per federated-config.md.

Cage block

The cage block declares filesystem, network, state, seccomp, and resource policy. It is the operator-readable contract enforced by the sandbox supervisor (sandbox.md).

cage:
  fs:                            # required, list (may be empty)
    - mode: ro|rw
      path: /abs/path
  net:                           # required
    allow: [<hostname-glob>, ...]
  state: ephemeral|scratch       # required
  seccomp: default|relaxed       # optional, default: default
  limits:                        # optional
    memory_mb: 256
    cpu_shares: 1024
    pids: 64
    walltime_sec: 600

cage.fs (list, required)

Filesystem mount declarations. Each entry is a {mode, path} object.

  • Required field, but may be empty (fs: [] = no host filesystem visible; cage sees only its own tmpfs).
  • Entries are evaluated in order. Later entries override earlier ones if paths conflict.
  • Modes:
    • ro — read-only bind mount into the cage.
    • rw — read-write bind mount. The subagent can modify the file at the resolved path.
  • path is always relative to the project root (per ADR-0011):
    • Absolute paths (/etc/foo, /srv/anything) are rejected at parse time. Projects are portable; absolute host paths are operator-machine-specific. To grant access outside the project root, see "What if a subagent needs outside-root access?" below.
    • Path must NOT escape the project root via .. segments.
    • Leading ./ is optional. ./data and data resolve identically.
    • The resolved path is <project-root>/<path>. Daemon checks existence at session-start (not parse-time, because paths may be created by earlier subagent runs or by operator setup).
  • No ~ expansion. Paths are project-relative; ~ would be a host concept the project cannot meaningfully assume.
  • No environment variable interpolation in path. Security boundary (see ADR-0006).

What if a subagent needs outside-root access?

Three options, in order of preference:

  1. Restructure the project so the data the subagent needs is inside the project root (move it in, symlink it in at the local-machine level, copy it). Keeps the project portable.
  2. Local symlink. The operator symlinks an external path into the project root on their machine (ln -s /etc/kubeconfig ./.local-only/kubeconfig). The DSL only knows about the project-relative path. Other operators receiving the project create their own equivalent symlink (or don't, if they don't have that local resource — the subagent then fails on missing path, which is honest).
  3. cage: disabled on that subagent (per ADR-0009 amendment). The subagent runs as the daemon UID with full host access. Visible in the DSL; operator-reviewable.

There is no fourth option that makes a non-portable subagent secretly portable. If a subagent needs /etc/kubeconfig, it's not portable, and that's a visible operator decision in the project file.

cage.net (object, required)

Network policy.

  • Required.
  • allow (list of strings, required) — hostname or hostname:port allowlist. Empty list (allow: []) means no network access at all.

Hostname syntax:

Pattern Matches
example.com exactly example.com, any port
example.com:443 exactly example.com on port 443 only
*.example.com any single subdomain of example.com (e.g. api.example.com) — does not match example.com itself or a.b.example.com
**.example.com any depth of subdomain of example.com
10.0.0.0/8 CIDR — explicit IP range (use sparingly; prefer hostnames)
localhost:6443 the cage's own loopback on port 6443 (note: localhost in a netns is the cage, not the host)
  • Resolution and filtering happen in the network gatekeeper at the cage layer (sandbox.md). The DSL only declares the allowlist; the gatekeeper enforces it.
  • DNS: allowlisted hostnames are resolved by a kaged-managed resolver. Non-allowlisted DNS queries fail with NXDOMAIN.
  • Port restriction: if no :port is given, all TCP ports to that hostname are allowed. If a port is given, only that port.

cage.state (enum, required)

Lifecycle of the cage's own filesystem (tmpfs, scratch space).

  • ephemeral — the cage's writable scratch (/tmp, /work, anything not mounted from host) vanishes when the subagent exits. The default for unit-of-work subagents.
  • scratch — the writable scratch persists between invocations within the same session, but is wiped when the session ends.
  • No persistent value in v1. True cross-session persistence requires the operator to declare an rw mount, which is explicit and auditable.

cage.seccomp (enum, optional)

Seccomp policy.

  • default (default) — the kaged-shipped conservative profile is applied. Blocks ptrace, kexec_load, init_module, keyctl, host mount, and similar host-impacting syscalls.
  • relaxed — only catastrophic syscalls (reboot, kexec_load) are blocked. Use only when a subagent fails under default and the operator has audited why.

There is no per-syscall override in v1. If the default profile bites a legitimate workload, the path forward is either (a) document the issue and we update the default, or (b) the operator opts into relaxed and accepts the wider surface.

cage.limits (object, optional)

Resource limits. Enforced via cgroups (see ADR-0009 and sandbox.md).

  • memory_mb (integer) — memory cap in megabytes. Default: 256.
  • cpu_shares (integer) — relative CPU weight. Default: 1024.
  • pids (integer) — max process count inside the cage. Default: 64.
  • walltime_sec (integer) — wall-clock timeout. After this, the supervisor SIGTERMs, then SIGKILLs after a 5s grace. Default: 600 (ten minutes).
  • A subagent that exceeds a limit is killed; an audit event records which limit was hit.

plugins (project-level registry, reinstated)

The top-level plugins: block was temporarily removed by ADR-0023 in favor of per-agent-only declarations. It has been reinstated as the project-level plugin registry. The rationale:

  1. Not all plugins are agent-related — some are UI plugins, task runners, or project-scoped services.
  2. A project-level registry provides a single place to declare source (where to install from) and project-wide defaults.
  3. Per-agent blocks now only override or disable; they cannot introduce new plugins.

Migration from per-agent-only (ADR-0023 era):

Per-agent-only:

primary:
  plugins:
    memory:
      package: "@kaged/memory-markdown"
      hooks: [on_session_start]
      config: { store: "config:/memory" }

Registry + per-agent override:

plugins:
  memory:
    package: "@kaged/memory-markdown"
    source: "npm:@kaged/memory-markdown"
    enabled: true
    config: { store: "config:/memory" }

primary:
  plugins:
    memory:
      enabled: true
      hooks: [on_session_start]

Install-time behavior: when the daemon loads the project and encounters a plugin in the registry, it checks the operator's local plugin store. Missing plugins with a source field trigger the install prompt. Missing plugins without source are reported but cannot be auto-installed.

Local-store state Registry says Daemon behavior
Not installed source: present Prompts operator: "Install plugin X from source Y?" Operator approves → install. Declines → plugin stays pending.
Not installed source: absent Reports as missing; no install prompt.
Installed matches Activate; no prompt.

tasks (named-object map, optional)

Operator-declared runnable commands for the project — build scripts, test suites, dev servers, deploy commands. Tasks appear as one-click buttons in the project UI. The full task-runner subsystem is specified in task-runner.md; this section covers the DSL surface only.

tasks:
  test:
    command: bun test
    description: Run the test suite
    group: ci
  dev:
    command: bun run dev
    description: Start the dev server
    group: dev
    long_running: true
  deploy-staging:
    command: ./scripts/deploy.sh staging
    description: Deploy to staging
    group: deploy
    confirm: true
  db-migrate:
    command: bun run db:migrate
    description: Run database migrations
    cwd: packages/api
  • Keys are task names (slugs). Pattern: ^[a-z][a-z0-9_-]{0,30}[a-z0-9]$. Reserved names (adhoc, all, new) are parse errors. Setting a key to null in project.local.yaml removes the task (ADR-0015 nullification).
  • Max entries: 64 per project.
  • Optional. Absence means no named tasks; the operator can still run ad-hoc tasks via the UI.

tasks.<name>.command (string, required)

The shell command to execute. Passed verbatim to the system shell — no DSL-level interpolation.

tasks.<name>.description (string, optional)

Human-readable description shown in the UI. Max 280 characters.

tasks.<name>.group (string, optional)

Organizational group for UI display. Tasks in the same group are visually clustered. Pattern: ^[a-z][a-z0-9_-]{0,30}[a-z0-9]$. Does not affect execution.

tasks.<name>.cwd (string, optional)

Working directory override, project-relative. Default: project root. Same path rules as cage.fs[].path — no absolute paths, no .. escape. Existence checked at task-launch time, not parse time.

tasks.<name>.long_running (boolean, optional)

Hint that this task is a long-lived process (dev server, watcher). Default: false. Affects UI only (stop button vs. "waiting for exit").

tasks.<name>.confirm (boolean, optional)

Prompt the operator for confirmation before launching. Default: false. Use for destructive or expensive operations.

tasks.<name>.env (object, optional)

Extra environment variables. Merged on top of the daemon's environment (additive, not replacing). Values are strings; no interpolation.


Validation timing

The DSL is parsed and validated at four moments. Each is intentional; conflating them would hide errors or fail healthy projects.

When What's checked Failure mode
kaged dsl validate <file> (CLI, on demand) Schema, alias name shape, path shape (relative, no ..), agent tree depth limit (≤16), principal_scope tag enforcement (kaged.issue.*/kaged.workflow.* rejected on non-root agents), root agent cage must be disabled Exit code non-zero with line/col
Daemon project-load (POST /api/v1/projects/load) All of the above, plus: alias resolution against operator's local config, plugin presence in local store, prompt-file existence, project-reference compilation (cycles, depth, nested DSL validity) Project enters pending or invalid per local-config.md; not a hard daemon failure
Daemon startup for known projects (re-evaluation) Same as project-load Project's state is updated in the registry; daemon stays up
Session-start All of the above, plus: model provider reachable, plugin processes can spawn, cage path existence inside project root Session refuses to start with specific error; project state unchanged

The first is purely file-level (an author validating their DSL).

The second pulls in the operator's local config (does this operator have the aliases bound, are the plugins installed). A pending project is not a broken project — it's a project that needs operator setup. The UI surfaces what's missing.

The third runs at daemon startup so the project list in kaged status and the UI reflects reality even after operator local config changes.

The fourth runs at the moment a session starts and pulls in runtime state (LLM provider reachable, plugin process healthy, host paths exist).

Error messages

Errors are human-first. Every error includes:

  • The file and line number of the offending construct (YAML parsers track this; we preserve it).
  • The kind of error (schema violation, cross-reference miss, cycle, etc.).
  • The expected shape, where applicable.
  • A pointer to the relevant doc (see docs/specs/project-dsl.md#field).

Examples:

.kaged/project.yaml:14:9
  primary.subagents.scraper.cage.fs[0].path: required path "/data" does not exist on this host
  Sandbox spawn requires all mount sources to exist.
  see docs/specs/project-dsl.md#cagefs

.kaged/project.yaml:22:5
  primary.subagents.scraper.tools."kaged.issue.create": tool "kaged.issue.create" carries
  principal_scope "root-only" and cannot be enabled on non-root agents.
  see docs/specs/project-dsl.md#tool-resolution

.kaged/project.yaml: top-level field "subagnets" is unknown (did you mean "subagents"?)
  Unknown fields are errors in strict mode (see ADR-0006).

No error is "an error occurred." Every error is actionable.

Worked examples

See ../dsl/examples/:

File What it shows
minimal.yaml The smallest valid DSL (primary only, no subagents).
single-subagent.yaml One subagent with a tight cage and per-agent tools.
multi-subagent.yaml Three subagents in a recursive tree (parent dispatches children).
network-allowlist.yaml Several net.allow patterns: globs, ports, CIDRs.
with-plugins.yaml Project declaring plugins with source and version.
nested-agents.yaml Recursive AgentSpec tree demonstrating per-agent cage and tools at multiple depths.
insecure.yaml A subagent with cage: disabled and the operator-readable warning header.
portable.yaml Best-practice portable project: aliases, project-relative paths, plugin declared with source.

Each example is a real .kaged/project.yaml file that kaged dsl validate accepts.

CLI surface

This spec defines what the following commands accept and what they output. The CLI implementation lives in packages/dsl/cli/.

kaged dsl validate <path>

Validates a DSL file. Exits 0 on success, non-zero with errors on failure.

  • <path> can be a file (./project.yaml) or a directory (the tool looks for .kaged/project.yaml inside it).
  • --strict (default): unknown fields are errors. Match the daemon's behavior.
  • --lenient: unknown fields produce warnings. Useful for forward-compat sniffing. Never the daemon's mode.
  • --json: emit errors as JSON for editor integration.

kaged dsl migrate <path> --to <version>

Migrates a DSL file to a different schema major version. Writes the migrated file to stdout (or to <path> with --in-place).

  • Errors with a diff if no migration path exists.

kaged dsl schema [--version N]

Prints the JSON Schema for the requested version. Defaults to the current version. Useful for piping into editor configs.

Failure modes

Failure Where caught Behavior
YAML syntax error parser line/col in error, no other validation runs
Unknown top-level field parser, strict mode error with did-you-mean suggestion
Missing required field schema error naming the field and its expected type
Type mismatch schema error with expected vs got
Agent tree depth exceeds 16 levels parser error: "agent tree depth exceeds the 16-level limit; flatten the tree or split into project references"
Root agent cage is not disabled parser error: "root agent cage must be disabled (interim restriction); see ADR-0022"
kaged.issue.* or kaged.workflow.* tool on non-root agent parser error: "tool carries principal_scope 'root-only' and cannot be enabled on non-root agents"
Path in system_prompt or cage.fs.path is absolute parser error: "paths in the DSL are project-root-relative; see ADR-0011"
Path uses .. to escape project root parser error: "paths must not escape the project root"
model value contains : (provider-style) parser error: "model fields are aliases; concrete provider:model bindings live in local config"
model alias name fails pattern parser error with the pattern rule
Path-existence failure (system_prompt or cage.fs.path) project-load (prompts) or session-start (cage paths) project enters pending (prompts) or session refuses to start (cage)
Alias unresolved in operator's local config project-load project enters pending; UI prompts operator to bind
Plugin in plugins not installed in local store project-load project enters pending; install prompt offered
Plugin version mismatch project-load install/upgrade prompt with diff
Unsupported version parser error with current daemon's supported version range
Duplicate subagent key schema error pointing at both definitions
cage: disabled on any non-root agent parser warning, not error; names agent + links to ADR-0009
cage: value that is neither object nor the literal disabled schema error with the two accepted forms
Subagent value mixes path: with cage: / model: / system_prompt: / tools: schema error: "this entry has path: (project reference) and also fields belonging to an AgentSpec declaration; pick one form"
Project-reference path: uses unsupported scheme (git:/, https:/, file:/, config:/) parser error: "v1 accepts project:/ only for nested project references; see project-dsl.md#subagentsnamepath"
Project-reference path: is naked (no prefix) parser error: naked path; see ADR-0015
Project-reference path: uses .. escape parser error: same .. escape rule as other URI paths
Project-reference path: resolves outside the parent project root project-load project enters invalid; error names the offending reference and the resolved path
Project-reference target directory has no .kaged/project.yaml project-load project enters pending; error names the missing file
Project-reference cycle (A → B → A or transitive) project-load (compilation pass) project enters invalid; error names the cycle path
Project-reference depth exceeds 16 levels project-load (compilation pass) project enters invalid; error: compile_depth_exceeded with the chain
Project-reference overrides contains version: or project: parser error: project identity cannot be overridden by parent; see project-dsl.md#subagentsnameoverrides
Project-reference overrides produces an invalid nested DSL when merged project-load project enters invalid; error references the merged-result validation failure
Project-reference name: collides with a sibling subagent's map key cross-ref pass error: "tool-name override conflicts with another subagent's identity; pick a unique value"

Open questions

These are tracked here rather than blocking the spec:

  1. Prompt frontmatter schema. The DSL's system_prompt field points at a markdown file. Whether/how that file carries metadata (model overrides, prompt versioning, dependencies) is a separate spec (prompts.md — TBD).
  2. Plugin allowlisting per subagent. v1 grants plugins project-wide. A per-agent plugins_allowed: [...] field on AgentSpec is plausible if needed; not in v1.
  3. Primary cage scheduling. The root agent's cage is restricted to disabled (interim). When the supervisor gains the ability to cage the primary process, this restriction will be lifted and the root agent will accept a full cage block. Tracked separately from the depth-16 limit and AgentSpec shape, which are stable.
  4. Hot reload. Can the daemon reload a project after its DSL changes mid-session? Probably yes for non-breaking changes (prompt edits, parameter tweaks) and no for breaking changes (agent name changes, cage tightening, project-reference graph changes). Detailed semantics in daemon.md when written.
  5. Remote project references. path: git:/... / path: https:/... for project references depend on ADR-0015 §4 (security ceiling) which is currently deferred per federated-config.md. Added in a later amendment.
  6. Cross-project composition beyond in-tree. In-tree composition via project-reference subagents (see Project-reference subagents) covers the local case. Cross-daemon mesh (git:/, https:/ references, ADR-0015 §6 parent injection) is deferred to v2.

Testing notes

Per ADR-0003, the first code PR for the DSL package lands failing tests. The test corpus this spec implies:

  • Schema conformance tests: every field-level rule in this doc has at least one test asserting "this file accepts" and one asserting "this file rejects with this error."
  • AgentSpec recursive tests: the recursive AgentSpec shape is tested at depths 1, 2, 8, and 16 (the limit). Depth 17 is a parse error. Per-agent tools, cage, model, system_prompt, and subagents fields are tested at each level. principal_scope enforcement (kaged.issue.*/kaged.workflow.* rejected on non-root) is tested at root vs depth-1 vs depth-2.
  • Error message tests: every error message named in Error messages is exercised by a test that asserts the exact line/col and the doc link.
  • Round-trip tests: for migration, every supported v1 → vN migration has a fixture pair (input + expected output).
  • Worked-example tests: every file in ../dsl/examples/ is validated as part of CI. If an example breaks the schema, CI fails.
  • Project-reference tests: the union under subagents.<name> must accept both shapes and reject mixed entries. Cycle detection over project references (including transitive A → B → C → A) is exercised end-to-end via fixture projects. Override-merge tests verify ADR-0015 semantics on the nested-project layer (deep merge, nullification, version/project rejection, schema validation on the merged result). Compiled output is verified to be a uniform AgentSpec subtree with _source annotations.

Amendments

2026-05-21 — cage: disabled opt-out

Added the string-form value "disabled" to subagents.<name>.cage to support the per-subagent sandbox opt-out introduced in ADR-0009 amendment. Field semantics, JSON Schema, failure-modes table, and cage_defaults interaction all updated.

Also added the insecure.yaml example demonstrating the opt-out with operator-readable warnings inline.

2026-05-21 — Portability: model aliases + project-relative paths + plugin install-on-load

Per ADR-0011, the DSL is now strictly portable. Three substantive changes:

  1. Model fields are aliases, not provider:model identifiers. primary.model and subagents.<name>.model are alias names (no :) resolved through the operator's local config. Concrete provider:model bindings live only in local-config.md. New ModelAlias schema type added to Appendix A.
  2. All path fields are project-root-relative. primary.system_prompt, subagents.<name>.system_prompt, and cage.fs[].path reject absolute paths and .. escapes. New ProjectRelativePath schema type added. The "what if a subagent needs outside-root access?" section documents the three options (restructure, symlink, cage: disabled).
  3. Plugins gain source and version fields. Project plugins can declare where to fetch from if not installed locally; kaged prompts the operator at project-load time per ADR-0008 amendment.

Validation timing gained a fourth moment ("project-load") to handle alias resolution and plugin presence checks against operator local config. Failure modes table extended. Examples in ../dsl/examples/ updated; new portable.yaml added.

2026-05-24 — tools: per-tool override map

Added the optional top-level tools: field — a named-object map (per ADR-0015) keyed by dot-delimited tool names (e.g. file.read, code.lsp). Values are ToolOverride objects (enabled, description, parameters — all optional) or null (ADR-0015 nullification to reset inherited overrides).

This field aligns with the ToolConfigMap / ToolOverride types implemented in @kaged/agent-tooling (packages/agent-tooling/src/tool-config.ts). The daemon's resolveToolConfig() deep-merges config layers (built-in defaults → project.yamlproject.local.yaml → parent injection) with nullification support.

Changes:

  1. Top-level shape gains tools: with a YAML example.
  2. New ### tools field section documenting enabled, description, parameters sub-fields, resolution order, and interaction with cage policies.
  3. JSON Schema gains tools property (object with additionalProperties referencing ToolOverride) and ToolOverride $def.
  4. Constrained by gains ADR-0015.

2026-05-25 — Array fields → named-object maps (subagents, tasks, plugins, interconnect)

Converted four top-level fields from object[] arrays to Record<string, object> named-object maps. The key serves as the item's identifier; the name field is removed from each sub-schema value. This enables ADR-0015 nullification: setting a key to null in project.local.yaml removes that entry from the merged config.

Changes:

  1. Top-level shape updated: subagents, tasks, plugins, interconnect shown as named-object maps with key descriptions.
  2. subagents section rewritten: keyed by subagent name; name field removed from value; propertyNames pattern and reserved-name exclusion moved to the map key.
  3. interconnect section rewritten: keyed by operator-chosen label; no name field in value.
  4. plugins section rewritten: keyed by plugin name (matching kaged-plugin.yaml name:); name field removed from value.
  5. New tasks narrative section added to Fields area: keyed by task name; name field removed from value; all sub-fields documented.
  6. JSON Schema (Appendix A): all four top-level properties changed from array/items to object/additionalProperties with oneOf [ref, null]; propertyNames patterns and maxProperties limits added; Subagent and PluginRef $defs have name removed from required and properties.
  7. Stale array references (subagents[], plugins[], etc.) updated throughout error messages, failure modes, open questions, amendments, and cross-ref note.
  8. All example files in docs/dsl/examples/ converted from array syntax to named-object map syntax.
  9. Dogfood config (.kaged/project.yaml) updated.

No migration support — pre-alpha, new format enforced directly.

2026-05-26 — Project-reference subagents (nested projects exposed as subagents)

Added the project-reference value shape under subagents.<name>. A subagent value may now be either:

  1. A full Subagent declaration (existing behavior), or
  2. A ProjectReference pointing at a nested kaged project via path: project:/.... The nested project's primary is exposed to the parent as a callable subagent.

This is the DSL surface for ADR-0015 §7 ("Compiled Contextualization"), which was previously marked deferred in federated-config.md. The two forms are distinguished by presence of path: — there is no explicit type: discriminator. Mixing fields from both forms is a parse error.

Changes:

  1. Top-level shape updated to show both subagent value forms side-by-side under subagents.
  2. New ### Project-reference subagents section added between subagents.<name>.parameters and the cage block. Documents:
    • The path field (URI-prefixed, project:/ only in v1).
    • The name field (optional tool-name override presented to the LLM; map key remains canonical).
    • The description field (optional tool-description override).
    • The overrides field (partial ProjectDsl deep-merged on top of the nested project; ADR-0015 merge semantics; version and project forbidden).
    • Cage policy for nested projects (nested cages stay nested; cage_defaults does not cross the silo boundary; parent tightens via overrides).
    • Cross-reference and call-graph semantics (project-refs participate by map key; nested primary is the entry point).
    • Compilation pass at project-load time with arbitrary depth and cycle detection.
    • What this is not (not hot reload, not cross-machine, not the §6 parent-injection mechanism).
  3. JSON Schema (Appendix A) gains a ProjectReference $def; subagents.additionalProperties.oneOf extended to include it.
  4. Failure modes table extended with eleven new entries covering shape-mixing, scheme restrictions, cycle detection, override identity collisions, and load-time existence checks.
  5. No code changes in this PR. Per ADR-0003, the schema mirror in @kaged/dsl (packages/dsl/src/schema.ts), failing tests, and parser support land in a follow-up PR. The spec is the contract; the code follows.

Cross-spec: federated-config.md un-defers ADR-0015 §7 in the same amendment, cross-linking to this section as the implementation surface.

Open questions deferred to a later amendment:

  • Remote project references (git:/, https:/) — gated on ADR-0015 §4 security ceiling enforcement.
  • Parent injection of virtual subagents into a child (ADR-0015 §6) — distinct mechanism, still deferred.
  • Hot reload semantics for nested-project DSL changes mid-session (belongs to daemon.md).

2026-05-26 — Recursive AgentSpec, per-agent tools/cage (ADR-0022)

Per ADR-0022, the DSL agent model is unified around a single recursive AgentSpec type. The former PrimaryAgent / Subagent distinction is removed — differentiation is now purely positional (root vs non-root).

Structural changes:

  1. AgentSpec replaces PrimaryAgent + Subagent. One recursive type used at every agent position. cage is required on all agents (was absent on PrimaryAgent). can_be_called_by is removed — the tree structure is the call graph.
  2. tools: moved from project-level to per-agent on AgentSpec. Each agent declares its own tool surface. The former top-level tools: section is removed.
  3. cage_defaults removed. No inheritance between cages; each agent declares its own cage independently.
  4. interconnect removed. Sibling and cross-tree calls do not exist; a parent calls its direct children only.
  5. Root agent interim restriction. Root agent's cage must be disabled. Non-root agents accept the full cage block.
  6. principal_scope enforcement. kaged.issue.* and kaged.workflow.* tools carry principal_scope: "root-only". The schema rejects them on non-root agents.
  7. subagents moved inside AgentSpec. Recursive nesting with depth limit 16. The top-level subagents property is removed; subagents live under primary.subagents.

Schema changes:

  • JSON Schema (Appendix A): PrimaryAgent and Subagent $defs replaced by single AgentSpec $def. Top-level required drops subagents. cage_defaults, interconnect, tools removed from top-level properties. Interconnect $def removed. AgentSpec carries tools and subagents as optional properties with recursive $ref.
  • Failure modes table: removed can_be_called_by and interconnect rows; added depth-limit, root-cage, and principal_scope rows.
  • Open questions: removed cage_profiles, interconnect event taxonomy, cross-project interconnect; added primary cage scheduling.
  • Validation timing: updated CLI check to include depth limit and principal_scope enforcement.
  • Testing notes: updated to cover recursive depth, principal_scope, and compiled AgentSpec subtree.
  • Worked examples table: updated to reflect new example names (defaults.yamlnested-agents.yaml, descriptions updated).

No migration support — pre-alpha, new format enforced directly.

2026-05-27 — Recursive tool materialization

Tool materialization now applies to every agent in the tree, not just the root. When availableTools is provided to compileProjectDsl(), every AgentSpec — root, inline subagents, and project-reference primaries — gets a materialized tools: { <name>: { enabled: true/false } } block.

Changes:

  1. compileAgent() now materializes tools at every level. Materialization moved from the post-walk phase in compileProjectDsl() into the recursive compileAgent() walker. Each agent gets its own materialized tools based on its position (root vs non-root) and its own tools: block.
  2. Non-root agents start with nothing enabled. The root agent uses DEFAULT_ENABLED_TOOLS as its base state; all subagents use an empty set. Subagents must explicitly opt in to every tool via their own tools: block.
  3. resolveRootTools() accepts optional defaultEnabled parameter. Third parameter defaults to DEFAULT_ENABLED_TOOLS for backward compatibility. The compiler passes an empty set for non-root agents.
  4. Operator overrides apply to all agents. The operatorToolOverrides layer is threaded through the entire agent tree, so operator-level tool policy is consistent across root and subagents.
  5. Project-reference primaries are treated as root. When crossing a project-reference boundary, the nested project's primary agent gets root-level defaults (kaged.* enabled), matching the semantics of a standalone project.

2026-05-27 — Default-disabled non-kaged tools

Only kaged.* tools (8 total: checkpoint, issue.*, ask, form) are enabled by default on the root agent. All other namespaces (file, search, code, debug — 10 tools) start disabled and require explicit opt-in via operator config ([default_tools]) or project DSL (primary.tools).

Changes:

  1. DEFAULT_ENABLED_TOOLS constant added to @kaged/dsl. A ReadonlySet<string> of the 8 kaged.* tools that are on by default. Exported from packages/dsl/src/defaults.ts.
  2. resolveRootTools() base state changed. Was enabled = true for all tools; now enabled = DEFAULT_ENABLED_TOOLS.has(name). Non-kaged tools must be explicitly opted in via an override layer with { enabled: true }.
  3. DEFAULT_ROOT_TOOLS semantics clarified. The constant is the canonical list of known tools (31 total), not the enabled set. Comments updated to reflect this.
  4. Spec language updated. Role-based default tools section and tool resolution section now document the opt-in model for non-kaged namespaces.

2026-05-27 — Compile-time tool resolution with operator overrides

Tool resolution for the root agent is now performed at compile time inside compileProjectDsl() rather than at runtime in the harness or daemon.

Changes:

  1. DEFAULT_ROOT_TOOLS constant added to @kaged/dsl. Canonical list of 18 built-in tools across 6 namespaces (file, search, code, debug, shell, kaged). Exported from packages/dsl/src/defaults.ts.
  2. resolveRootTools() function added to @kaged/dsl. Accepts available tool names and an ordered list of override layers (operator-level from local.toml [default_tools], then project-level from DSL primary.tools). Returns the filtered list of enabled tool names. Later layers override earlier ones; null or { enabled: false } disables a tool; { enabled: true } re-enables it.
  3. CompileProjectDslOptions extended. New optional fields: availableTools (registered tool names from the runtime) and operatorToolOverrides (from local config default_tools).
  4. CompileProjectDslResult extended. New field: resolvedRootTools: string[] | null — the effective tool list when availableTools was provided, or null when omitted.
  5. Tool resolution section updated. Documents the four-layer resolution order (available → operator → project → cage) and the compile-time resolution flow.
  6. Daemon primary-runner.ts updated. Now uses resolveRootTools() with operator default_tools as the first layer and DSL primary.tools as the second, replacing the former filterEnabledTools() call.
  7. ToolOverrideLayer type exported from @kaged/dsl. Reusable type for override maps: Record<string, { enabled?: boolean } | null>.

2026-06-03 — Agent execution limits: max_steps and max_output_tokens

Per agent-harness runtime work: Mastra's default maxSteps is 5 and the default maxOutputTokens is provider-dependent. Both are too low for complex tool-heavy workflows. These fields give the operator explicit control over the agent loop budget.

  1. max_steps added to AgentSpec. Optional integer 1100. Controls the maximum number of tool-call rounds the agent loop is allowed before stopping. Passed to Mastra Agent.stream() execution options.
  2. max_output_tokens added to AgentSpec. Optional integer 165536. Controls the maximum output tokens per completion. Passed to the provider as max_tokens (or provider-specific equivalent).
  3. parameters section updated. The parameters field (opaque model-specific parameters) is now documented with a note that it is separate from max_steps and max_output_tokens — the latter are kaged-managed execution limits, while parameters is the escape hatch for provider-specific knobs.
  4. AgentSpec shape snippet and JSON Schema (Appendix A) updated. Both fields added to the AgentSpec schema under properties.
  5. Testing notes updated. Added tests for: max_steps inheritance (subagent inherits parent value unless overridden), max_output_tokens override, out-of-range rejection (max_steps: 0 and max_output_tokens: 70000 are parse errors), and provider passthrough (values reach the harness runtime and Mastra).
  6. Constrained-by list updated. No new ADRs — this is an execution-limit surface, not an architectural decision. The fields are constrained by the existing ADR-0012 (Mastra substrate) and ADR-0014 (provider routing).

2026-05-27 — ADR-0023 & ADR-0024: per-agent plugins and compaction blocks

Per ADR-0023 and ADR-0024:

  1. Project-level plugins: block removed. The top-level plugins: key formerly accepted at project root is no longer valid. The parser emits a clear error pointing to ADR-0023 and showing the equivalent per-agent rewrite. This is a breaking schema change at version: 1; consistent with the pre-alpha "no migration" pattern.

  2. AgentSpec.plugins added. A new optional named-object map on every AgentSpec node carrying per-agent plugin declarations. Each entry: package (required on root declaration, inherited under isolation: project), isolation: 'agent' | 'project' (default 'agent', root only), hooks: [hook-name] (subscribed lifecycle hooks), config (opaque plugin-defined config). Max 16 plugins per agent. Per ADR-0023's no-inheritance posture, subagents do not inherit a parent's plugins under isolation: agent. Under isolation: project, subagents declare parallel plugins.<name> blocks with override fields.

  3. AgentSpec.compaction added. A new optional object on every AgentSpec node carrying per-agent context-compaction config. Fields: strategy ('drop' | 'summarize' | 'delegate' | 'checkpoint', default 'drop'), upper_threshold (default 0.85), lower_threshold (default 0.60), always_keep, and strategy-specific subfields (summarize, delegate, checkpoint). Subagents inherit field-by-field from parent's compaction:; overrides at the subagent level only override specified fields.

  4. Parse-time validations added.

    • Project-level plugins: rejection. Clear error pointing to ADR-0023 migration.
    • AgentSpec.plugins.<name>.package required on root declarations; forbidden on subagent declarations under isolation: project (since package is inherited).
    • isolation is root-only. Setting isolation on a subagent's plugin declaration is a parse error.
    • compaction.lower_threshold < compaction.upper_threshold enforced.
    • compaction.delegate.plugin must reference a declared plugin in the same agent's plugins: block AND that plugin must claim role: compactor in its manifest (validated at project-load time, not parse time, since the manifest is needed).
    • hooks: [on_session_start | on_session_idle] on a subagent emits a parse-time warning (the hook is primary-only; declaring it on a subagent is permitted but it will never fire).
  5. AgentSpec shape snippet updated to show the two new fields between tools: and subagents:.

  6. Top-level shape example updated to show plugin declarations on the primary (the common case).

  7. Appendix A JSON Schema to be updated (this entry tracks the spec amendment; the schema update is a follow-up patch).

  8. Constrained-by list extended with ADR-0023 and ADR-0024.

2026-06-06 — Default compaction summary prompt, summarize.model optional, auto-creation on init

Per ADR-0024 2026-06-06 amendment (compaction hardening):

  1. summarize.model is now optional. When omitted, the summarizer uses the agent's own model alias. Previously it was required. This makes the default compaction config usable without any explicit configuration — operators get summarize-at-threshold for free.

  2. summarize.prompt default defined. The default prompt path is config:/prompts/compaction-summary.md. Previously prompt was required when strategy: summarize. Now it has a default, so the summarize block can be entirely implicit in the fallback config.

  3. Default fallback config extended. The no-config fallback now includes a summarize block with default values (prompt: config:/prompts/compaction-summary.md, window_messages: 20, preserve_recent: 10, max_summary_tokens: 1500). This enables the drop strategy's summarize-at-threshold upgrade (ADR-0024 amendment item 5) without any operator configuration.

  4. Default compaction summary prompt section added. New § Default compaction summary prompt section documenting the auto-created config:/prompts/compaction-summary.md file: its content (structured handoff format), auto-creation behavior (kaged project init and project-load fallback), and override mechanism.

  5. AgentSpec shape snippet updated. summarize comment changed from "strategy-specific" to "optional, defaults provided" to reflect the new optionality.

2026-05-31 — Plugin registry model: top-level plugins reinstated as project registry

  1. Project-level plugins: reinstated as the plugin registry. Every plugin used in the project MUST be declared here first. This is the gateway — it makes plugins available to agents and other project services. Per-agent plugins: blocks only override or disable; they cannot introduce new plugins.

  2. PluginRefSchema updated. Fields: package (string, required), source (string, optional, magic-prefix validated: npm:, github:, project:/, config:/, git:), enabled (boolean, optional, default false), config (object, optional). version removed.

  3. AgentPluginDeclSchema simplified. Fields: enabled (boolean, optional), hooks (list, optional), config (object, optional). package and isolation removed — package lives in the registry only; isolation is now a plugin-config key, not a DSL schema field.

  4. Cross-reference validation updated. Per-agent slot names must reference a key in the project-level plugins: registry. Unknown slots produce a cross_reference error. The old isolation: project inheritance validation removed entirely.

  5. isolation moves to plugin config. Plugins that support isolation (e.g. memory-markdown) accept isolation: agent | project as a config key. The DSL schema no longer has a first-class isolation field. This is more flexible — each plugin defines its own scoping semantics.

  6. source field semantics defined. Optional field in the registry entry. Tells the daemon where to fetch the plugin if it's not installed. Five prefix patterns supported. Missing plugins without source are reported but cannot be auto-installed.

  7. Appendix A JSON Schema PluginRef updated to reflect new fields.


Appendix A — JSON Schema (v1)

This is the normative schema. Published at https://kaged.dev/schema/v1.json and shipped inside the kaged binary.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://kaged.dev/schema/v1.json",
  "title": "kaged project DSL v1",
  "type": "object",
  "additionalProperties": false,
  "required": ["version", "project", "primary"],
  "properties": {
    "version": { "const": 1 },
    "project": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$"
    },
    "description": {
      "type": "string",
      "maxLength": 280
    },
    "primary": { "$ref": "#/$defs/AgentSpec" },
    "plugins": {
      "type": "object",
      "additionalProperties": {
        "oneOf": [
          { "$ref": "#/$defs/PluginRef" },
          { "type": "null" }
        ]
      },
      "propertyNames": {
        "minLength": 1
      },
      "description": "Named-object map of plugins keyed by plugin name. Null values remove the plugin (ADR-0015 nullification)."
    },
    "tasks": {
      "type": "object",
      "maxProperties": 64,
      "additionalProperties": {
        "oneOf": [
          { "$ref": "#/$defs/Task" },
          { "type": "null" }
        ]
      },
      "propertyNames": {
        "pattern": "^[a-z][a-z0-9_-]{0,30}[a-z0-9]$",
        "not": { "enum": ["adhoc", "all", "new"] },
        "description": "Task name. See tasks section."
      },
      "description": "Named-object map of tasks keyed by task name. Null values remove the task (ADR-0015 nullification)."
    }
  },
  "$defs": {
    "AgentSpec": {
      "type": "object",
      "additionalProperties": false,
      "required": ["model", "system_prompt", "cage"],
      "properties": {
        "model": { "$ref": "#/$defs/ModelAlias" },
        "system_prompt": { "$ref": "#/$defs/ProjectRelativePath" },
        "cage": {
          "oneOf": [
            { "$ref": "#/$defs/Cage" },
            { "const": "disabled" }
          ]
        },
        "description": {
          "type": "string",
          "maxLength": 280
        },
        "parameters": { "type": "object" },
        "tools": {
          "type": "object",
          "additionalProperties": {
            "oneOf": [
              { "$ref": "#/$defs/ToolOverride" },
              { "type": "null" }
            ]
          },
          "propertyNames": {
            "pattern": "^[a-z][a-z0-9]*([.][a-z][a-z0-9]*|[.][*])?$",
            "description": "Dot-delimited tool name (e.g. 'file.read') or namespace glob ('debug'). See agent-tooling.md."
          },
          "description": "Per-agent tool configuration overrides. Keys are tool names or globs; values are ToolOverride objects or null (ADR-0015 nullification)."
        },
        "subagents": {
          "type": "object",
          "maxProperties": 64,
          "additionalProperties": {
            "oneOf": [
              { "$ref": "#/$defs/AgentSpec" },
              { "$ref": "#/$defs/ProjectReference" },
              { "type": "null" }
            ]
          },
          "propertyNames": {
            "pattern": "^[a-z][a-z0-9_]{0,30}[a-z0-9]$",
            "not": { "enum": ["primary", "operator", "system"] },
            "description": "Agent name. See subagents section."
          },
          "description": "Named-object map of child agents keyed by name. Values may be an AgentSpec (recursive), a ProjectReference (nested project — presence of `path` is the discriminator), or null (ADR-0015 nullification). Depth limit: 16 levels."
        }
      },
      "description": "The single recursive agent shape used at every position in the tree. See § AgentSpec."
    },
    "ProjectReference": {
      "type": "object",
      "additionalProperties": false,
      "required": ["path"],
      "properties": {
        "path": {
          "type": "string",
          "pattern": "^project:/(?!/).+$",
          "description": "URI-prefixed path to a nested project root. v1 accepts only the project:/ scheme; the path portion must not contain '..' escapes. See the project-reference subagents section."
        },
        "name": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]{0,30}[a-z0-9]$",
          "not": { "enum": ["primary", "operator", "system"] },
          "description": "Optional tool-name override presented to the LLM. Defaults to the nested project's slug. Does not change the map key."
        },
        "description": {
          "type": "string",
          "maxLength": 280,
          "description": "Optional tool-description override presented to the LLM. Defaults to the nested project's description."
        },
        "overrides": {
          "type": "object",
          "description": "Partial ProjectDsl deep-merged on top of the nested project's resolved config. 'version' and 'project' are forbidden; otherwise any field accepted by ProjectDsl may appear. Merge follows ADR-0015 §2 (deep merge, null nullifies, arrays replace). Schema validation runs on the merged result, not on this partial."
        }
      }
    },
    "ModelAlias": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]{0,62}[a-z0-9]$",
      "not": { "enum": ["primary", "subagent", "operator", "system", "default"] },
      "description": "Model alias name. Cannot contain ':'. See ADR-0011 and local-config.md."
    },
    "ProjectRelativePath": {
      "type": "string",
      "pattern": "^(?!/)(?!.*\\.\\.(\/|$)).+$",
      "description": "Path relative to project root. No leading slash; no '..' segments."
    },
    "Cage": {
      "type": "object",
      "additionalProperties": false,
      "required": ["fs", "net", "state"],
      "properties": {
        "fs": {
          "type": "array",
          "items": { "$ref": "#/$defs/Mount" }
        },
        "net": { "$ref": "#/$defs/Net" },
        "state": { "enum": ["ephemeral", "scratch"] },
        "seccomp": { "enum": ["default", "relaxed"], "default": "default" },
        "limits": { "$ref": "#/$defs/Limits" }
      }
    },
    "Mount": {
      "type": "object",
      "additionalProperties": false,
      "required": ["mode", "path"],
      "properties": {
        "mode": { "enum": ["ro", "rw"] },
        "path": { "$ref": "#/$defs/ProjectRelativePath" }
      }
    },
    "Net": {
      "type": "object",
      "additionalProperties": false,
      "required": ["allow"],
      "properties": {
        "allow": {
          "type": "array",
          "items": { "type": "string" }
        }
      }
    },
    "Limits": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "memory_mb": { "type": "integer", "minimum": 16 },
        "cpu_shares": { "type": "integer", "minimum": 1 },
        "pids": { "type": "integer", "minimum": 1 },
        "walltime_sec": { "type": "integer", "minimum": 1 }
      }
    },
    "PluginRef": {
      "type": "object",
      "additionalProperties": false,
      "required": ["package"],
      "properties": {
        "package": { "type": "string", "minLength": 1 },
        "source": { "type": "string", "minLength": 1, "pattern": "^(npm:|github:|project:/|config:/|git:)" },
        "enabled": { "type": "boolean" },
        "config": { "type": "object" }
      }
    },
    "ToolOverride": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean", "default": true },
        "description": { "type": "string" },
        "parameters": { "type": "object" }
      },
      "description": "Per-tool configuration override. All fields optional. See agent-tooling.md for the built-in tool catalog."
    }
  }
}

Structural validations beyond JSON Schema's expressiveness (agent tree depth limit, root agent cage: disabled interim restriction, principal_scope enforcement for kaged.issue.*/kaged.workflow.* on non-root agents, project-reference cycle detection) are performed by the kaged parser as a post-validation pass.

References