# Migrating from tiles to plugins

Tessl is transitioning from **tiles** to **plugins** as the packaging format for shareable agent rules and skills. This document explains why, what changed, and how to migrate your existing tile packages.

## Why the change?

Tessl started with tiles: prepackaged bundles of context for agents. Since then, the wider agent ecosystem is settling on plugins as the standard format for the same idea, so we're moving Tessl onto plugins.

For you, this means what you build in Tessl travels naturally to different coding agents, and the terminology lines up with what your teams are already seeing. For us, it's a stronger foundation to build on. As agents start handling richer kinds of context, plugins give us the base to layer versioning, distribution, evaluation, and security on top of these new context types.

***

## What changed

### Manifest location and filename

| Format | File          | Location                    |
| ------ | ------------- | --------------------------- |
| Tile   | `tile.json`   | Package root                |
| Plugin | `plugin.json` | `.tessl-plugin/plugin.json` |

The manifest moves into a dedicated `.tessl-plugin/` directory. This keeps the root clean and makes space for sibling agent manifests (`.claude-plugin/`, `.cursor-plugin/`) without naming collisions, if and when they are added.

### Field-by-field comparison

| Concept           | tile.json                                 | plugin.json               | Notes                                          |
| ----------------- | ----------------------------------------- | ------------------------- | ---------------------------------------------- |
| Package name      | `name`                                    | `name`                    | Same format: `workspace/package`               |
| Version           | `version`                                 | `version`                 | Required at publish in both                    |
| Short description | `summary`                                 | `description`             | Field renamed; trimmed and required at publish |
| Private flag      | `private`                                 | `private`                 | Identical                                      |
| Repository URL    | `repository`                              | `repository`              | Both accept `https://` URLs                    |
| Package author    | —                                         | `author`                  | New: `{ name, email, url }` object             |
| Homepage          | —                                         | `homepage`                | New: URL string                                |
| License           | —                                         | `license`                 | New: SPDX identifier, e.g. `"MIT"`             |
| Skills            | `skills: { "name": { path: "..." } }`     | `skills: "./skills/"`     | Changed from keyed object to path(s)           |
| Rules / Steering  | `rules: { "name": { rules: "..." } }`     | `rules: "./rules/"`       | Changed from keyed object to path(s)           |
| Commands          | `commands: { "name": { script: "..." } }` | `commands: "./commands/"` | Changed from keyed object to path(s)           |
| Docs file         | `docs: "./README.md"`                     | —                         | Removed; move content into skills or rules     |
| Describes (PURL)  | `describes: "pkg:..."`                    | —                         | Removed; not carried forward                   |

### Skills: from keyed registry to directory discovery

In `tile.json`, each skill had an explicit key and path:

```json
{
  "skills": {
    "my-skill": { "path": "./skills/my-skill/SKILL.md" },
    "other-skill": { "path": "./skills/other-skill/SKILL.md" }
  }
}
```

In `plugin.json`, you point at a directory and Tessl discovers skills automatically. A skill's identity comes from the `name:` field in its `SKILL.md` frontmatter (falling back to the directory basename if no frontmatter is present):

```json
{
  "skills": "./skills/"
}
```

You can also pass an explicit array of paths when you want precise control:

```json
{
  "skills": ["./skills/my-skill/SKILL.md", "./skills/other-skill/SKILL.md"]
}
```

### Rules: from nested objects to directory paths

In `tile.json`:

```json
{
  "rules": {
    "style-guide": { "rules": "./rules/style-guide.md" }
  }
}
```

Note: `steering` was accepted as an alias for `rules` in `tile.json` and normalised automatically. Both wrote to the same field internally.

In `plugin.json`:

```json
{
  "rules": "./rules/"
}
```

All `.md` files found in the declared directory are included as rules. There are no longer named keys; the rule name is derived from the filename.

### Docs and Describes: removed

The `docs` field (a standalone markdown file) and `describes` field (a PURL for a versioned dependency) have no equivalent in `plugin.json`.

* **`docs`**: Fold the content into a rule or skill. If the doc was user-facing guidance about a tool or library, it belongs as a skill.
* **`describes`**: This field declared that a tile described a specific versioned package (e.g. a Go library). It was used as part of Docs, as Docs have not been carried forward, neither has Describes.

If you publish with `TESSL_PLUGIN=1` and your `tile.json` still has `docs` or `describes` set, the CLI will warn you at pack time that these fields will not be included.

For converting docs to skills, there is a [skill available in the Tessl Registry](https://tessl.io/registry/tessleng/doc-skill-creator).

***

## Compatibility during the transition

Tiles and plugins coexist. The CLI continues to read and publish `tile.json` packages without any changes required. No existing tile breaks.

When both a `tile.json` and a `.tessl-plugin/plugin.json` are present in the same package `.tessl-plugin/plugin.json` takes precedence for metadata (name, version, description).

***

## How to migrate

Using the migrate command

The CLI can generate `.tessl-plugin/plugin.json` from your existing tile.json automatically:

```
tessl tile migrate
```

Run this from your package root (the directory containing tile.json). The command:

* Creates `.tessl-plugin/` if it doesn't exist
* Translates all supported fields (name, version, summary → description, repository, private, skills, commands, rules)
* Warns if your tile.json contains docs or describes — these have no plugin equivalent and will not appear in the output

If you want to migrate a package that isn't in the current directory, pass the path:

```
tessl tile migrate ./path/to/my-package
```

What the command does not do

The migrate command handles the mechanical translation. A few steps still require your attention:

* docs and describes: If your tile.json uses these fields, the command will warn you and omit them. See Docs and Describes: removed for how to fold that content into skills or rules.
* SKILL.md frontmatter: The command copies your skill paths as-is. If your SKILL.md files are missing a name: field, Tessl will fall back to the directory basename — but adding frontmatter is recommended. See Step 4: Add SKILL.md frontmatter.
* tile.json is kept: The command does not delete your existing tile.json. Once you've verified the new manifest with tessl lint and tessl publish --dry-run, you can remove it manually.

***

### Alternatively:

### Step 1: Create `.tessl-plugin/plugin.json`

Create the directory and manifest:

```
mkdir .tessl-plugin
touch .tessl-plugin/plugin.json
```

Translate your `tile.json` fields:

**tile.json (before):**

```json
{
  "name": "myorg/my-package",
  "version": "1.2.0",
  "summary": "Linting rules for our TypeScript monorepo",
  "repository": "https://github.com/myorg/my-package",
  "skills": {
    "lint-fix": { "path": "./skills/lint-fix/SKILL.md" }
  },
  "rules": {
    "ts-style": { "rules": "./rules/ts-style.md" },
    "imports": { "rules": "./rules/imports.md" }
  }
}
```

**.tessl-plugin/plugin.json (after):**

```json
{
  "name": "myorg/my-package",
  "version": "1.2.0",
  "description": "Linting rules for our TypeScript monorepo",
  "repository": "https://github.com/myorg/my-package",
  "skills": "./skills/",
  "rules": "./rules/"
}
```

### Step 2: Check your directory layout

The path values in `plugin.json` are relative to the **package root** (i.e. the directory containing `.tessl-plugin/`). Verify that the paths you declared actually exist:

```
my-package/
├── .tessl-plugin/
│   └── plugin.json          ← manifest
├── skills/
│   └── lint-fix/
│       └── SKILL.md         ← discovered automatically
└── rules/
    ├── ts-style.md           ← discovered automatically
    └── imports.md            ← discovered automatically
```

### Step 3: Handle docs and describes

If your `tile.json` has a `docs` field:

1. Open the file it points to.
2. If the content is a step-by-step procedure for the agent to follow, convert it to a skill: create a `SKILL.md` in your skills directory with the content.
3. If the content is constraints or style guidance, move it into your rules directory as a `.md` file.
4. Remove `docs` from `tile.json` (or skip it entirely if you're dropping `tile.json`).

### Step 4: Add SKILL.md frontmatter (recommended)

With `tile.json`, skill identity was the key in the `skills` object. With `plugin.json`, identity comes from the `name:` field in `SKILL.md` frontmatter. Add frontmatter if it's missing:

```markdown
---
name: lint-fix
description: Fix lint errors in TypeScript files using the project's oxlint config
---

# Lint Fix

...
```

If there's no frontmatter, Tessl uses the directory basename as the skill name — so existing skills continue to work without changes.

### Step 5: Verify with the CLI

Run the linter to catch any issues:

```
tessl lint
```

In plugin mode (`TESSL_PLUGIN=1`), the CLI will:

* Warn if `tile.json` is still present with `docs` or `describes` fields that have no plugin equivalent.
* Validate that every path declared in `plugin.json` resolves and contains recognisable content.

Do a dry-run pack to confirm what will be included in the tarball:

```
tessl publish --dry-run
```

### Step 6: Publish

```
tessl publish
```

Once you're confident in the new format, you can delete `tile.json` from your repository to remove the duplication.

***

## Running evaluations after migration

`tessl eval run` works the same way it did for tiles: point it at the package root. The only change is where that root is.

Before migration, you pointed at the tile directory (where `tile.json` lives):

```
tessl eval run <path-to-tile>
```

After migration, point at the plugin directory (where `.tessl-plugin/plugin.json` lives):

```
tessl eval run <path-to-plugin>
```

The plugin directory is the *parent* of `.tessl-plugin/`, not `.tessl-plugin/` itself. From a plugin root, this is usually just `.`:

```
tessl eval run .
```

***

## Quick reference

### tile.json → plugin.json field mapping

| tile.json                                  | plugin.json                                |
| ------------------------------------------ | ------------------------------------------ |
| `summary`                                  | `description`                              |
| `skills.X.path`                            | `skills` (directory or array)              |
| `rules.X.rules`                            | `rules` (directory or array)               |
| `steering.X.rules`                         | `rules` (directory or array)               |
| `commands.X.script`                        | `commands` (directory or array)            |
| `docs`                                     | No equivalent — move to skills or rules    |
| `describes`                                | No equivalent — contact us                 |
| `name`, `version`, `private`, `repository` | Identical                                  |
| —                                          | `description` (required at publish)        |
| —                                          | `author`, `homepage`, `license` (optional) |

### Minimum valid plugin.json

```json
{
  "name": "workspace/package-name",
  "version": "1.0.0",
  "description": "One sentence describing what this plugin does",
}
```

By convention, skills will be pulled from the `./skills` directory and rules from `./rules`. If they are not set, these conventions will be used.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.tessl.io/use/tile-to-plugin-migration.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
