# Adl

> Author and maintain projects built with ADL (Agent Definition Language) - the YAML manifest format from `inference-gateway/adl` that `adl-cli` turns into a full A2A agent scaffold (Go/Rust/TypeScript). Use when working inside a generated agent project (presence of `agent.yaml` + `.adl-ignore`), when editing the manifest, when implementing custom tools and skills against the generated scaffold, or when planning the schema-first / domain modelling for a new agent.

- Skill: `inference-gateway/adl` (Agent Skill)
- Install (CLI): `npx skillmds@latest add inference-gateway/adl`
- Raw SKILL.md: https://api.skillmd.com/api/skills/inference-gateway/adl/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: Apache-2.0
- Author: inference-gateway (https://skillmd.com/u/inference-gateway)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/inference-gateway/adl

---


# ADL (Agent Definition Language) Expert

Use this skill when working in any project whose root contains an `agent.yaml`
manifest with `apiVersion: adl.inference-gateway.com/v1` (or `v2`+ when it
ships) together with an `.adl-ignore` file. That pair is the fingerprint of a
project generated by [`adl-cli`](https://github.com/inference-gateway/adl-cli)
from a schema in [`inference-gateway/adl`](https://github.com/inference-gateway/adl).
Also use it when the user is authoring a fresh manifest (`adl init`) or
planning the domain model for a new agent.

## Mental model

ADL is "OpenAPI for AI agents". A single declarative manifest names everything
the agent needs - capabilities, the AI provider, services, function-call
tools, markdown skills, server/auth, language runtime, sandbox, deployment -
and `adl-cli` turns it into an enterprise-ready, A2A-compatible project. You
own the manifest and the bodies of TODO placeholders; the CLI owns everything
else and will regenerate it on demand.

**The contract has two halves you must keep straight:**

| Concept                                                                          | Where it lives    | Who owns it                      | Regenerated on `adl generate`?  |
| -------------------------------------------------------------------------------- | ----------------- | -------------------------------- | ------------------------------- |
| Manifest (`agent.yaml`)                                                          | repo root         | you                              | no - source of truth            |
| Scaffolding (`main.go`, `config/`, `internal/<service>/`, `Dockerfile`, CI, ...) | various           | the generator                    | yes                             |
| Custom tool bodies (`tools/<name>.{go,rs,ts}`)                                   | `tools/`          | you, after a TODO scaffold       | no - protected by `.adl-ignore` |
| Custom service impls (`internal/<service>/*.go`)                                 | `internal/`       | you, after a TODO scaffold       | no - protected by `.adl-ignore` |
| Skills (`.agents/skills/<id>/SKILL.md`)                                          | `.agents/skills/` | you (bare) or upstream (sourced) | no - whole dir protected        |

If you forget which half a file belongs to, `cat .adl-ignore` - anything
matched there survives `adl generate --overwrite`.

**Spec at a glance.** Every `spec.*` top-level field the v1 schema defines:

| `spec.*` field  | Required? | Purpose                                                                                               |
| --------------- | --------- | ----------------------------------------------------------------------------------------------------- |
| `capabilities`  | yes       | A2A feature flags - `streaming`, `pushNotifications`, `stateTransitionHistory` (all three booleans)   |
| `server`        | yes       | `port` (1-65535), optional `scheme`, `debug`, `auth.enabled`, `authz` (enabled + mode)                |
| `language`      | yes       | At least one of `go`, `typescript`, `rust` (each with its own required pair, e.g. `module`+`version`) |
| `agent`         | no        | LLM provider/model/systemPrompt/maxTokens/temperature + `mcp` MCP client (servers + runtime config)   |
| `card`          | no        | Static A2A agent-card metadata + advertised security schemes (A2A section 7)                          |
| `services`      | no        | Domain services declared as ports (`type`, `interface`, `factory`, `description`)                     |
| `config`        | no        | Arbitrary per-section config maps; one section per service (env-mapped)                               |
| `tools`         | no        | Function-call entrypoints - reserved built-in ids and user tools                                      |
| `skills`        | no        | Markdown playbooks - registry / GitHub `source:` / `bare: true`                                       |
| `acronyms`      | no        | String list the generator preserves in generated identifier casing                                    |
| `artifacts`     | no        | `enabled: true` to generate an artifacts server (filesystem or MinIO backend)                         |
| `telemetry`     | no        | `enabled: true` for OpenTelemetry; `traces`/`metrics` select per-signal exporters (Go/TS only)        |
| `hooks`         | no        | `post: [...]` commands the CLI runs after each `adl generate`                                         |
| `scm`           | no        | `provider`, `url`, `github_app`, `issue_templates`, `dependabot`, `ci`, `cd`                          |
| `documentation` | no        | `pages[]` with `title`, `path`, and optional `description` - hand-authored docs seeded in `docs/`     |
| `examples`      | no        | `title` and `description` - seeded once as `examples/<slug>/README.md`, linked from the README        |
| `development`   | no        | `sandbox.{flox,devcontainer,dockerCompose}` + `ai.orchestrators.{claudecode,...}` + `deps[]`          |
| `deployment`    | no        | `type: kubernetes \| cloudrun \| vercel \| cloudflare` plus the matching block                        |

The sections below cover each in turn. Anything not in this table is not in
v1 - if you see it in an existing manifest, treat it as a CLI extension and
verify against `adl-cli`'s changelog.

## Schema-first / domain-first workflow

ADL is schema-driven by design: the manifest is the domain model. Sequence
work so the manifest leads and the code follows.

1. **Model the domain in YAML before touching any source file.**
   - Name the bounded context in `metadata.name` (lowercase, hyphenated) and
     pin `metadata.version` (semver - `^\d+\.\d+\.\d+$`). Optional metadata:
     `author` (`name` required, `email`/`url`), `license` (same SPDX enum as
     skills, or `Proprietary`), and `tags[]` for discoverability.
   - **Declare the required spec frame first:** `spec.capabilities` (all
     three booleans - `streaming`, `pushNotifications`,
     `stateTransitionHistory`), `spec.server.port`, and at least one
     `spec.language.{go|typescript|rust}` target. Almost always set
     `spec.agent` (provider + model) and `spec.card` (A2A discovery) too -
     they're optional in the schema but the agent is useless without them.
   - Enumerate **services** (`spec.services.*`) as ports: each gets a
     `type` (`service`/`repository`/`client`/`middleware`), an `interface`,
     a `factory`, and a description. One service per responsibility - don't
     conflate `database` and `cache`.
   - Enumerate **config sections** (`spec.config.*`) as value objects. Use
     dotted-name injection (`config.database`) to give a tool _only_ the
     subsection it needs.
   - Enumerate **tools** (`spec.tools[]`) as commands - the verbs the model
     can call. Each one declares its JSON Schema (`schema:`) and the services
     it depends on (`inject:`).
   - Enumerate **skills** (`spec.skills[]`) as the procedural knowledge - the
     "how" and "when" for using the tools. Skills are markdown, not code.
2. **Validate the manifest before generating.** `adl validate agent.yaml`
   checks shape against the pinned schema and rejects typos in reserved
   namespaces (e.g. `spec.config.tools.bash.tymeout_seconds`).
3. **Generate.** `adl generate --file agent.yaml --output .` writes the
   scaffold. The first run creates `.adl-ignore` with every file containing a
   TODO marked for protection. Re-run with `--overwrite` to refresh
   non-protected scaffolding after a manifest edit.
4. **Implement TODO bodies.** Each generated `tools/<name>.{go,rs,ts}` and
   each `internal/<service>/*.{go,rs,ts}` ships with `// TODO:` markers
   spelling out what to replace. The factory signature and the handler
   signature are locked - don't rename them, or you'll desync from the
   generator's expectations on the next regeneration of `main.go`.
5. **Test what you implemented** (see "Testing custom tools" - this is the
   non-negotiable part).
6. **Run.** `task build && task run` (Go/Rust) or the equivalent in the chosen
   language. The generated `Taskfile.yml` is the entry point.

## Capabilities, the LLM, and the agent card

Three top-of-spec blocks shape what the agent advertises, which model it
talks to, and how clients discover it. Set them before anything domain-
specific.

**`spec.capabilities` (required).** All three booleans must be present -
the validator rejects the manifest if any is missing:

```yaml
capabilities:
  streaming: true # SSE-based streaming responses
  pushNotifications: false # webhook callbacks on long-running tasks
  stateTransitionHistory: true # record task state transitions for replay
```

**`spec.agent` (optional but near-universal).** The LLM the generated agent
defers to. Provider is a fixed enum: `openai`, `anthropic`, `ollama`,
`deepseek`, `google`, `mistral`, `groq`, `cohere`, `cloudflare`, `moonshot`,
`ollama_cloud`, `nvidia`, `minimax`, or `""` for "configure at runtime via
env vars only". Temperature is bounded to 0-2; `maxTokens` must be ≥1.

```yaml
agent:
  provider: deepseek
  model: deepseek-v4-flash
  systemPrompt: |
    You are a helpful A2A agent. Use the AVAILABLE SKILLS playbooks for
    workflows; call tools for deterministic actions.
  maxTokens: 4096
  temperature: 0.3
```

**`spec.agent.mcp` (optional).** Configuration for the ADK's built-in MCP
(Model Context Protocol) client: the `servers` the agent connects to at runtime
to discover and call external tools (on top of the locally generated
`spec.tools`), plus the global runtime settings for that client. `enabled` is
the **required master switch** (maps to `A2A_MCP_ENABLED`) - when `false` (the
default) no MCP client is generated or wired in, even if `servers` lists
entries. Only meaningful for an LLM-backed agent, which is why it lives under
`spec.agent`.

Each `servers[]` entry requires `name` (unique, `^[a-zA-Z0-9_-]+$`) and
`transport` (`stdio` | `sse` | `http`): `stdio` launches a local subprocess
(`command`, `args`, `env`); `http`/`sse` connect to a remote endpoint (`url`,
`headers`). Note the Go ADK client is **HTTP-only with a single shared
connection/retry set** - the runtime fields below apply globally across all
servers, not per-server, and the server base URLs it dials (`A2A_MCP_SERVERS`)
are derived from `servers`:

```yaml
agent:
  provider: anthropic
  model: claude-sonnet-5
  mcp:
    enabled: true # required master switch -> A2A_MCP_ENABLED
    endpoint: /mcp # path appended to each server URL -> A2A_MCP_ENDPOINT
    refreshInterval: 5m # tool re-discovery cadence -> A2A_MCP_REFRESH_INTERVAL
    dialTimeout: 30s # connect timeout -> A2A_MCP_DIAL_TIMEOUT
    callTimeout: 30s # per-call timeout -> A2A_MCP_CALL_TIMEOUT
    maxRetries: 0 # 0 = retry forever -> A2A_MCP_MAX_RETRIES
    retryInterval: 2s # initial backoff -> A2A_MCP_RETRY_INTERVAL
    retryMaxInterval: 30s # backoff ceiling -> A2A_MCP_RETRY_MAX_INTERVAL
    servers:
      - name: filesystem
        transport: stdio
        command: npx
        args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
      - name: internal-api
        transport: http
        url: https://mcp.example.com/mcp
        headers:
          Authorization: Bearer ${MCP_TOKEN}
```

Every runtime field maps 1:1 to an `A2A_MCP_*` env var, and the manifest value
becomes the generated default (e.g. in `.env.example`); the env var overrides
it at runtime. `enabled` is the only required field - omit the rest to take the
defaults shown. (Restructured in adl v0.23.0 / adl-cli v0.54.0; the older flat
`spec.agent.mcps[]` list no longer validates. The env switches were renamed
from `_ENABLE` to `_ENABLED` in adl v0.24.1.)

**`spec.card` (optional).** Static fields for the A2A agent card served at
`/.well-known/agent-card.json`. Used by other agents and the Inference
Gateway registry for discovery. The base fields are free-form
strings/arrays:

```yaml
card:
  protocolVersion: "0.3.0"
  preferredTransport: JSONRPC
  defaultInputModes: [text, voice]
  defaultOutputModes: [text, audio]
  url: "https://my-agent.example.com:8443"
  documentationUrl: "https://github.com/company/my-agent/docs"
  iconUrl: "https://github.com/company/my-agent/icon.png"
```

Since adl v0.24.0 the card also carries the A2A section-7 auth surface:

- `supportsExtendedAgentCard: true` makes the generated ADK serve the
  authenticated `GET /extendedAgentCard` endpoint (with the A2A error
  contract for unsupported/misconfigured calls). Defaults to false.
- `securitySchemes` declares named schemes in flat OpenAPI-3.0 authoring
  form - `type: apiKey` (plus `name` and `in: query|header|cookie`),
  `type: http` (plus `scheme`, optional `bearerFormat`), or
  `type: mutualTLS`. OIDC/OAuth2 are deliberately **not** modelled here:
  they are runtime concerns (`AUTH_ISSUER_URL` / `AUTH_CLIENT_ID` /
  `AUTH_CLIENT_SECRET` env) and the ADK derives their declaration at
  startup.
- `security` lists the advertised requirements, each entry mapping a scheme
  name from `securitySchemes` to its required scopes (empty list for
  scope-less schemes). OpenAPI semantics: keys within one entry are ANDed,
  separate array entries are ORed.

```yaml
card:
  protocolVersion: "0.3.0"
  supportsExtendedAgentCard: true
  securitySchemes:
    apiKey:
      type: apiKey
      name: X-API-Key
      in: header
    bearer:
      type: http
      scheme: Bearer
      bearerFormat: JWT
  security:
    - apiKey: []
    - bearer: []
```

## Server, auth, and language targets

**`spec.server` (required).** Only `port` is mandatory. `scheme` (`http`/
`https`), `debug`, `auth`, and `authz` are optional. `auth` is the
authentication on/off toggle - the concrete provider (OIDC, JWT, ...) lives
in the generated code and config sections, not the schema:

```yaml
server:
  port: 8443
  scheme: https
  debug: false
  auth:
    enabled: true
  authz:
    enabled: true
    mode: deny-all
```

**`spec.server.authz` (adl v0.25.0).** Authorization, as distinct from
authentication. `enabled: true` scaffolds a **user-owned BeforeTool
authorization callback** in the generated project; `mode` sets the default
policy until you implement custom logic - `allow-all` (the default),
`deny-all`, or `custom` (you must write the logic yourself). Both fields are
optional; omit the whole block and no authz scaffold is generated. Pair
`authz` with the card's `securitySchemes`/`security` block so what the agent
enforces matches what its card advertises.

**`spec.language` (required).** At least one of three children; the
generator emits the corresponding scaffold. Required pairs per language:

| Language     | Required fields                     | Optional     |
| ------------ | ----------------------------------- | ------------ |
| `go`         | `module`, `version`                 | -            |
| `typescript` | `packageName`, `nodeVersion`        | -            |
| `rust`       | `packageName`, `version`, `edition` | `features[]` |

```yaml
language:
  go:
    module: github.com/example/my-agent
    version: "1.26.7"
```

**`vendor.{deps,devdeps}`.** Every language block accepts a `vendor` object
(schema-validated since v0.11): `deps[]` for runtime dependencies and
`devdeps[]` for dev/test-only tools, each entry in `<package>@<version>`
form using the language's native syntax. The manifest is **authoritative**:
since adl-cli v0.48.0 the generator rewrites `go.mod` / `Cargo.toml` /
`package.json` from it, so any dependency your custom code imports but does
not declare here is silently dropped on the next `adl generate`. Add extra
deps in the manifest - never by editing `go.mod` directly:

```yaml
language:
  go:
    module: github.com/example/my-agent
    version: "1.26.7"
    vendor:
      deps:
        - github.com/stretchr/testify@v1.10.0
      devdeps:
        - golang.org/x/tools/cmd/stringer@v0.20.0
```

For Go, `devdeps` become `tool` directives - CLI executables only (e.g.
`counterfeiter`, `stringer`). Libraries imported by `_test.go` files (e.g.
`testify`) belong in `deps`. Pair vendor deps with a
`spec.hooks.post: [go mod tidy]` hook (see
[Artifacts, telemetry, and post-generate hooks](#artifacts-telemetry-and-post-generate-hooks))
so the indirect dependency graph stays consistent after each regeneration.

## Tools vs Skills (the often-confused distinction)

ADL distinguishes two complementary surfaces, and conflating them produces
unmaintainable agents. Apply the rule first, then write the entry.

| Use a **tool** (`spec.tools[]`) when                                              | Use a **skill** (`spec.skills[]`) when                                        |
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| The agent must invoke a deterministic operation (DB query, HTTP call, file write) | The agent must learn a workflow, policy, or response pattern                  |
| Inputs and outputs are structured (JSON Schema fits)                              | The instructions are prose                                                    |
| Implemented in code                                                               | Authored as markdown                                                          |
| Registered with the toolbox at startup                                            | Loaded into the system prompt at startup via the `AVAILABLE SKILLS:` manifest |

A skill that needs to read files (e.g. its own `SKILL.md` body, or bundled
templates) requires `- id: read` in `spec.tools` _and_
`spec.config.tools.read.enabled: true`. The validator enforces this; don't
disable it.

## Reserved built-in tools

`spec.tools[]` recognises five reserved ids that map to framework-supplied
implementations. They ship with their own unit tests (see
`builtin/*_test.go.tmpl` in the CLI templates), so you do **not** need to
write tests for them. You activate them - that's all:

| Reserved id | Purpose                                                   | Activation namespace      |
| ----------- | --------------------------------------------------------- | ------------------------- |
| `read`      | Read a file (`file_path`, optional `offset`/`limit`)      | `spec.config.tools.read`  |
| `bash`      | Execute a whitelisted shell command with a timeout        | `spec.config.tools.bash`  |
| `write`     | Write content to a file (creates parent dirs)             | `spec.config.tools.write` |
| `edit`      | Replace a unique `old_string` with `new_string` in a file | `spec.config.tools.edit`  |
| `fetch`     | `GET`/`HEAD` an http(s) URL (host whitelist, byte cap)    | `spec.config.tools.fetch` |

All five default to `enabled: false`. Opt in by listing the id alone (no
`name`, `description`, or `schema` - the generator owns those) and setting
`enabled: true` in the matching `spec.config.tools.<id>` block. The reserved
config block accepts only the typed keys the generator knows; typos like
`tymeout_seconds` fail validation by design.

Resolution precedence at runtime is **env > compile-time literal > built-in
default (disabled)**. The kill-switch envs (`A2A_BASH_DISABLED=1`,
`A2A_FETCH_DISABLED=1`, etc.) override the compile-time `enabled: true`.

Each built-in accepts its own typed config keys under
`spec.config.tools.<id>`. Anything not on this list fails validation:

| Tool    | Config keys                                                                                                                                       |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `read`  | `enabled`, `max_lines` (default file slice), `allowed_roots[]` (empty = project-wide)                                                             |
| `bash`  | `enabled`, `whitelist[]` (allowed commands), `timeout_seconds`                                                                                    |
| `write` | `enabled`                                                                                                                                         |
| `edit`  | `enabled`                                                                                                                                         |
| `fetch` | `enabled`, `allowed_domains[]` (entries starting with `.` match any subdomain), `max_bytes`, `timeout_seconds`, `allow_downloads`, `download_dir` |

A representative configuration that opts in the three tools with the
richest config surfaces:

```yaml
config:
  tools:
    read:
      enabled: true
      max_lines: 2000
    bash:
      enabled: true
      whitelist: [ls, cat, grep, find, rg, jq, wc, head, tail, git, go]
      timeout_seconds: 30
    fetch:
      enabled: true
      allowed_domains:
        - pkg.go.dev
        - .rust-lang.org # any subdomain of rust-lang.org
        - raw.githubusercontent.com
      max_bytes: 5242880 # 5 MiB
      timeout_seconds: 20
      allow_downloads: true
      download_dir: /tmp/adl-fetch-cache
tools:
  - id: read
  - id: bash
  - id: fetch
```

## Custom tool implementation

A user tool is a full `spec.tools[]` entry - `id`, `name`, `description`,
optional `tags`, `inject`, and a JSON Schema for `schema`. The generator
produces `tools/<name>.{go,rs,ts}` with:

- a struct holding the injected dependencies (`logger`, services, optional
  `config` or `config.<section>` subsections),
- a constructor `New<PascalName>Tool(...) server.Tool` whose signature mirrors
  `inject:` in declaration order, and
- a handler method `<PascalName>Handler(ctx, args) (string, error)` whose body
  is a single `// TODO: Implement <name> logic` comment plus a placeholder
  return.

You replace the handler body. Do not change:

- the struct name or its field order (regeneration of `main.go` wires fields
  positionally),
- the constructor signature,
- the handler method name.

Injection patterns you can use in `inject:`:

```yaml
inject:
  - logger # always available, *zap.Logger (Go)
  - config # the whole *config.Config
  - config.database # only *config.DatabaseConfig - principle of least privilege
  - cache # any name declared in spec.services
```

Always prefer `config.<section>` over `config` - it keeps the tool's blast
radius small, makes unit tests trivial to set up, and the validator catches
mismatches between `inject:` and `spec.config.*` at generate time.

## Testing custom tools (mandatory)

**The built-in reserved tools (`read`, `bash`, `write`, `edit`, `fetch`) ship
with generated unit tests. Custom tools do NOT.** This is the single most
common gap in ADL projects.

Verification step for every PR that adds or modifies `spec.tools[]`:

1. For each entry whose `id` is _not_ in the reserved set above, confirm a
   sibling test file exists - `tools/<name>_test.go` (Go), `tools/<name>.rs`
   tests block (Rust), `tools/<name>.test.ts` (TypeScript) - and the file is
   listed in `.adl-ignore` so regeneration won't clobber it.
2. The test file MUST exercise the handler against a service mock for every
   dependency in `inject:`. Use the service's interface (declared in
   `spec.services.*.interface`) - that's why it exists.
3. Use table-driven tests (Go) / parameterised tests (Rust) / `describe`+`it`
   blocks (TypeScript) covering at least: happy path, invalid args, service
   error.
4. Run the language-native test command from the generated Taskfile:
   `task test` (Go and Rust) or the equivalent target. CI must pass.

A minimal Go template for a custom tool test (adapt to the actual struct
name and interface):

```go
package tools

import (
    "context"
    "errors"
    "testing"

    "go.uber.org/zap"
    "github.com/stretchr/testify/require"
)

type stubDatabase struct{ err error; out string }

func (s *stubDatabase) Query(ctx context.Context, sql string) (string, error) {
    return s.out, s.err
}

func TestQueryDatabaseTool_Handler(t *testing.T) {
    cases := []struct {
        name    string
        args    map[string]any
        db      *stubDatabase
        wantErr bool
    }{
        {"happy path", map[string]any{"query": "SELECT 1", "table": "t"}, &stubDatabase{out: `{"rows":1}`}, false},
        {"missing query", map[string]any{"table": "t"}, &stubDatabase{}, true},
        {"backend error", map[string]any{"query": "SELECT 1", "table": "t"}, &stubDatabase{err: errors.New("boom")}, true},
    }
    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            tool := &QueryDatabaseTool{logger: zap.NewNop(), database: tc.db}
            _, err := tool.QueryDatabaseHandler(context.Background(), tc.args)
            if tc.wantErr {
                require.Error(t, err)
            } else {
                require.NoError(t, err)
            }
        })
    }
}
```

Skip this and the agent ships untested business logic - the schema can't
catch a misread `args["query"].(string)` or a swallowed service error.

## The `.adl-ignore` contract

`adl-cli` writes this file on first generation. It works like `.gitignore`
but applies to the _generator_ - anything matched is preserved across
`adl generate --overwrite`. The generator automatically adds:

- every custom tool file (`tools/<name>.<ext>`),
- every custom service implementation (`internal/<service>/*`),
- every generated skill directory (`.agents/skills/<id>/`).

You can extend it to protect anything else you've hand-edited:

```text
# .adl-ignore
Dockerfile          # custom container build
k8s/                # using ArgoCD overlays elsewhere
Taskfile.yml        # extended with project-specific targets
README.md           # written by hand
```

Patterns: `#` for comments, trailing `/` for directories, `*` wildcards,
exact paths.

**When a regeneration goes wrong:** check `.adl-ignore` first. A missing
entry there is the usual cause of "my custom code disappeared after I edited
the manifest."

## Services and configuration

Services are first-class - declare them in `spec.services` with `type`,
`interface`, `factory`, and `description`, and the generator creates
`internal/<service>/<service>.{go,rs}` with an interface stub and a factory
that takes `(*zap.Logger, *config.Config)`.

`type` is a closed enum - pick the one that best names the role so the
generator can emit idiomatic scaffolding:

| `type`       | Use it for                                                  |
| ------------ | ----------------------------------------------------------- |
| `service`    | Domain logic / orchestration (default choice when in doubt) |
| `repository` | Persistence and data-access ports - DBs, object stores      |
| `client`     | Outbound HTTP / gRPC / RPC clients to another system        |
| `middleware` | Request-pipeline behaviour - auth, logging, rate limits     |

`interface` and `factory` must match `^[a-zA-Z][a-zA-Z0-9_]*$` - they
become real identifiers in the generated code. Three rules keep this
maintainable:

1. **One interface per responsibility.** Don't ship a `UtilService`. Split
   `database` and `cache` even if they share a backend.
2. **Inject the interface, never the implementation.** Tools depend on
   `database.DatabaseService`, not on `*database.databaseService`. This is
   what makes the unit-test stubs above trivial.
3. **Configuration mirrors services.** A service named `googleCalendar`
   reads its settings from `spec.config.googleCalendar` -> generated as
   `GoogleCalendarConfig` with `GOOGLE_CALENDAR_*` env prefix. Don't
   hand-wire env vars; let the prefix mapping do it.

## Skills inside an ADL agent

`spec.skills[]` accepts three entry shapes, resolved by `adl-cli`:

| Shape                                                                             | Behaviour                                                                                           |
| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `id: <name>` (and optional `version: <semver>`)                                   | Fetched from `https://registry.inference-gateway.com/skills/`. Override with `ADL_SKILLS_REGISTRY`. |
| `id: <name>` + `source: <shorthand-or-URL>`                                       | The whole GitHub directory (SKILL.md + any bundled assets) is pulled into `.agents/skills/<id>/`.   |
| `id: <name>` + `bare: true` (+ `name`, `description`, `tags`, optional `license`) | Scaffolded locally as a TODO. Author it by hand.                                                    |

`source:` shorthand:

```yaml
- id: skill-creator
  source: skill-creator # inference-gateway/skills, main
- id: skill-creator
  source: skill-creator@v1.0 # pinned tag
- id: pdf
  source: anthropics/skills/pdf # different repo
- id: pdf
  source: anthropics/skills/pdf@abc1234 # pinned commit SHA
- id: custom
  source: https://github.com/my-org/my-repo/tree/release/path/to/skill
```

At runtime, the generated agent walks first-level subdirectories under
`.agents/skills/` (override with `A2A_SKILLS_DIR`), parses each
`<id>/SKILL.md`'s frontmatter, and appends an `AVAILABLE SKILLS:` block to the
system prompt - the _bodies_ are not inlined. The model loads them on demand
via the `read` tool, so a skills-using agent must opt `read` in (see "Reserved
built-in tools"). The generator also symlinks `.claude/skills ->
../.agents/skills`, so Claude Code reads the same tree at
`.claude/skills/<id>/SKILL.md`. (adl-cli v0.52.2 moved generated skills from
`skills/` to `.agents/skills/`; existing projects must move the directory - or
set `A2A_SKILLS_DIR=skills` - on the next regenerate.)

`license:` on a skill entry must be one of the SPDX identifiers the schema
accepts (`MIT`, `Apache-2.0`, `BSD-2-Clause`, `BSD-3-Clause`, `GPL-2.0`,
`GPL-3.0`, `LGPL-2.1`, `LGPL-3.0`, `MPL-2.0`, `ISC`, `CC0-1.0`,
`CC-BY-4.0`, `CC-BY-SA-4.0`, `Unlicense`) or the literal `Proprietary`. SPDX
expressions like `MIT OR Apache-2.0` are not currently accepted.

## Local development: sandbox and AI assistants

`spec.development` configures the experience of working _on_ the agent
locally - reproducible dev environments and AI-assistant onboarding files.
Three independent subsections (`sandbox`, `ai`, `deps`), all optional.

**`spec.development.sandbox`.** Three alternative packagings - pick any
combination; each declares its own `enabled` boolean. Generated artefacts:

| Sub-block       | `enabled: true` generates                                                                      |
| --------------- | ---------------------------------------------------------------------------------------------- |
| `flox`          | `.flox/env/manifest.toml` (Flox/Nix-backed reproducible env)                                   |
| `devcontainer`  | `.devcontainer/devcontainer.json` (VS Code Dev Containers)                                     |
| `dockerCompose` | `docker-compose.yaml` (with the artifacts server wired in when `spec.artifacts.enabled: true`) |

**`spec.development.ai.orchestrators`.** Per-agent toggles for the coding
assistants the project is meant to be edited with, nested under
`ai.orchestrators` (the older flat `ai.<agent>` and `ai.enabled` shapes are
**rejected** by `adl validate`/`adl generate` with a migration hint - move
the toggle under `orchestrators`). Each is independent; all default off.
Enabling one generates its onboarding doc plus a GitHub Actions workflow,
kept in sync on `adl generate --overwrite` unless listed in `.adl-ignore`:

| Sub-block    | Generated docs file                            | Generated workflow             |
| ------------ | ---------------------------------------------- | ------------------------------ |
| `claudecode` | `CLAUDE.md` (Anthropic Claude Code)            | `.github/workflows/claude.yml` |
| `gemini`     | `GEMINI.md` (Google Gemini)                    | `.github/workflows/gemini.yml` |
| `codex`      | shared `AGENTS.md` (OpenAI Codex)              | `.github/workflows/codex.yml`  |
| `opencode`   | shared `AGENTS.md`                             | none (no upstream action yet)  |
| `infer`      | shared `AGENTS.md` (Inference Gateway `infer`) | `.github/workflows/infer.yml`  |

Enabling `claudecode` also provisions the `claude-code` CLI into the Flox /
DevContainer sandboxes automatically.

A small example combining sandbox + AI toggles:

```yaml
development:
  sandbox:
    flox:
      enabled: true
    devcontainer:
      enabled: false
    dockerCompose:
      enabled: true
  ai:
    orchestrators:
      claudecode:
        enabled: true
      codex:
        enabled: true
```

**`spec.development.deps[]`.** A cross-cutting list of sandbox-level tool
dependencies (`<package>@<version>`, e.g. `kubectl@1.31.0`) installed into
every enabled sandbox flavour - for tools that don't belong to any single
language's package manager.

## SCM, CI/CD, and deployment

Two related blocks that together drive everything outside `main.go`: where
the code lives, how it ships, and where it runs.

**`spec.scm`.** All fields optional; the provider enum is closed:

| Field             | Effect                                                                    |
| ----------------- | ------------------------------------------------------------------------- |
| `provider`        | `github` \| `gitlab` \| `bitbucket` - selects the workflow templates      |
| `url`             | Repository URL (used in generated `README.md`, agent card, etc.)          |
| `github_app`      | Generate a GitHub App / token configuration in CI                         |
| `issue_templates` | Write `.github/ISSUE_TEMPLATE/*.md`                                       |
| `dependabot`      | Write `.github/dependabot.yml`                                            |
| `ci`              | Write `.github/workflows/ci.yml`                                          |
| `cd`              | Write `.github/workflows/cd.yml` and `.releaserc.yaml` (semantic-release) |

When `github_app: true`, the generated CD workflow reads the App credentials
from repo secrets `RELEASER_APP_ID` / `RELEASER_APP_PRIVATE_KEY` by default;
override the names with `spec.scm.app_id_secret` / `app_private_key_secret`.
The `claudecode` and `infer` orchestrators take the same override pair -
`appIdSecret` / `appPrivateKeySecret`, defaulting to `CLAUDE_APP_*` /
`INFER_APP_*` (see `spec.development.ai.orchestrators`).

**`spec.deployment`.** Choose `type: kubernetes`, `cloudrun`, `vercel`, or
`cloudflare`; the matching sub-block carries the detail. `kubernetes` and
`cloudrun` deploy a prebuilt container image and share an `image` shape
(`registry`, `repository`, `tag`, optional `useCloudBuild`); `vercel` and
`cloudflare` deploy from source via the platform's own build pipeline, so
they have no `image` block. In any `environment:` map, use `${VAR}`
placeholders for secrets - never inline real values.

`type: kubernetes` generates `k8s/deployment.yaml`:

```yaml
deployment:
  type: kubernetes
  kubernetes:
    image:
      registry: ghcr.io
      repository: example/my-agent
      tag: v1.0.0
```

`type: cloudrun` generates `cloudrun/` helpers plus a `deploy` target in
the `Taskfile.yml`. Fields map 1:1 to Cloud Run concepts:

```yaml
deployment:
  type: cloudrun
  cloudrun:
    image:
      registry: gcr.io
      repository: my-agent
      tag: v1.0.0
      useCloudBuild: true
    resources:
      cpu: "1"
      memory: 512Mi
    scaling:
      minInstances: 0
      maxInstances: 100
      concurrency: 1000
    service:
      timeout: 3600
      allowUnauthenticated: true
      serviceAccount: my-agent@PROJECT_ID.iam.gserviceaccount.com
      executionEnvironment: gen2
    environment:
      LOG_LEVEL: info
      ENVIRONMENT: production
```

`type: vercel` deploys from source through Vercel's build pipeline. Fields:
`project`, `team`, `framework` (omit to auto-detect), `runtime` (`nodejs` |
`edge`), `regions[]`, `functions.{memory,maxDuration}`, `environment`:

```yaml
deployment:
  type: vercel
  vercel:
    project: my-agent
    runtime: nodejs
    regions: [iad1]
    functions:
      memory: 1024
      maxDuration: 300
    environment:
      LOG_LEVEL: info
```

`type: cloudflare` targets Cloudflare Workers (not Pages); the CLI
translates the block into wrangler configuration. Fields: `name`,
`accountId` (prefer a `${VAR}` placeholder), `compatibilityDate`
(`YYYY-MM-DD`; generator supplies a default if omitted),
`compatibilityFlags[]` (e.g. `nodejs_compat`), `routes[]`, `workersDev`,
`environment` (wrangler `vars`; real secrets go out-of-band via
`wrangler secret put`):

```yaml
deployment:
  type: cloudflare
  cloudflare:
    name: my-agent
    accountId: ${CLOUDFLARE_ACCOUNT_ID}
    compatibilityDate: "2026-01-01"
    compatibilityFlags: [nodejs_compat]
    routes:
      - agent.example.com/*
    workersDev: false
```

`adl generate` exposes equivalent CLI flags (`--ci`, `--cd`,
`--deployment kubernetes|cloudrun`, `--flox`, `--devcontainer`) that **OR
with the manifest values**. The `--deployment` flag only accepts
`kubernetes` and `cloudrun` - `vercel` and `cloudflare` are manifest-only. Prefer the manifest for anything that needs to
be reproducible across machines and CI runs - treat the flags as
one-off escape hatches.

## Documentation pages and examples

The generator owns two root-level docs, regenerated on every run: `README.md`
(overview, quick start, tools/skills/examples tables) and `CONFIGURATIONS.md`
(the full config reference - the custom `spec.config` table plus every `A2A_*`
env var, including the telemetry rows when enabled). The README's
Configuration section is just a short paragraph linking to `CONFIGURATIONS.md`.
Neither is in `.adl-ignore` by default; add them yourself if you fork them.

Two spec blocks enrich the generated README with hand-authored content.

**`spec.documentation.pages[]` (optional).** Declare hand-authored documentation
pages that link from the generated README. Each entry requires `title` and `path`
(the path relative to the repo root, typically `docs/<file>.md`); `description`
is optional. The generator creates a stub `docs/<file>.md` on first run (title-only,
never overwrites) and renders a `## Documentation` section in the README:

```yaml
documentation:
  pages:
    - title: Getting Started
      path: docs/getting-started.md
      description: Quickstart guide for the agent
    - title: Architecture
      path: docs/architecture.md
      description: System design and component overview
```

The stub files follow the same seed-once pattern as bare skill scaffolds - the
generator writes them only if they do not exist, so your edits survive
`adl generate --overwrite`. Add the `docs/` directory to `.adl-ignore` if you
want full control over the file set.

**`spec.examples[]` (optional).** Declare curated examples that link from the
generated README. Each entry has `title` and `description` only - there is no
`path` field. The generator derives a directory from the title
(lowercased, spaces to dashes) and seeds `examples/<slug>/README.md` once
(title + description + TODO, never overwritten); the README's `## Examples`
table links each entry to its directory. The `examples/` directory is listed
in `.adl-ignore`, so everything you add there survives regeneration:

```yaml
examples:
  - title: Basic Chat # -> examples/basic-chat/
    description: A simple request-response interaction
  - title: Multi-turn Workflow # -> examples/multi-turn-workflow/
    description: Chaining several tool calls across turns
```

Both blocks are purely additive: omitting them produces the same README as
before.

## Artifacts, telemetry, and post-generate hooks

Three small spec blocks that round out the manifest.

**`spec.artifacts.enabled`.** Set `true` to generate an artifacts server -
a small HTTP service for storing task outputs - and to 

…(truncated)
