Agent manifests
Write an agent or a flow as a folder of Markdown and JSON, check it offline, and save it with nylorun apply.
npx nylorun init agents/support
npx nylorun check agents/support
npx nylorun apply agents/support --agent
npx nylorun chat support --message "Why was I charged twice in March?"---
name: support
description: Answers billing questions in two sentences or fewer.
---
You answer questions about invoices and refunds. If a question is not about
billing, say so and stop.An agent manifest is written as files: AGENT.md with its name,
description, and instructions, and optional JSON files for its runtime, MCP
servers, HTTP tools, and, for a flow, flow.json. nylorun check compiles the
files offline into the manifest the Runtime saves (see
Saving manifests). nylorun apply sends them to
the Runtime, which compiles them again with the same compiler, saves the
manifest's next version, and keeps the files byte for byte as that version's
source. --agent then makes the
Agent support from the manifest, and nylorun chat
talks to it. Sessions run the saved manifest, never the files.
Versions
Manifests written as files need nylorun 0.20.0-beta, @nylorun/agents 0.28.0-beta, and
Runtime 0.32.0-beta or later. Runtime 0.31 compiles single agents only: it
refuses flow.json, agents/, plugins/, and extends. The file format is
agent-folder/1.
The loop
| Step | Command | Does |
|---|---|---|
| Write | nylorun init <path> [--description <text>] [--example] | Writes <path>/AGENT.md with its frontmatter and a line of instructions. The agent is named after the path's last segment. |
| Check | nylorun check [<dir>...] [--fix] [--json] | Compiles each folder with the Runtime's compiler and prints every problem by file, line, and column. Reads no Runtime. --fix makes the fixable edits; --json is for CI. |
| Save and run | nylorun apply [<dir>...] [--dry-run] [--if-match <n>] [--agent [<id>]] [--watch [--save]] | Checks each folder, plans the save on the Runtime, then saves it under If-Match. --dry-run prints the plan and the compiled manifest and saves nothing. --agent then makes or moves the Agent. |
| Talk | nylorun chat <agent> [--session <id>] [--message <text>] [--owner <user>] | Sends a message to the Agent and prints the reply as it comes. |
init <path> names the agent after the path's last segment: init agents/hello
writes agents/hello/AGENT.md for the agent hello. --example also writes
tools/get_order.json (an HTTP tool), mcp.json (a server with a
Bearer ${ORDERS_MCP_TOKEN} header), triggers/new-order.md (a webhook
trigger), and a README.md that explains each. The first run in a project,
the nearest directory with .git, also writes .gitattributes
(* text=auto eol=lf), adds .nylorun/credentials.json to .gitignore, and
maps the folder's JSON files to their schemas in
.vscode/settings.json, each only when it is missing:
$ npx nylorun init agents/hello --example
Wrote agents/hello/: AGENT.md, tools/get_order.json, mcp.json, triggers/new-order.md, README.md.
Wrote .gitattributes (* text=auto eol=lf), so a Windows checkout keeps LF line endings.
Added .nylorun/credentials.json to .gitignore.
Mapped the agent folder files to their JSON Schemas in .vscode/settings.json.
Next: nylorun check agents/hello, then nylorun apply agents/hello --agent to save it and make the Agent hello, then nylorun chat hello.check and apply take a folder that holds AGENT.md, or a folder of such
folders: npx nylorun check agents/ checks each subfolder with an AGENT.md.
check exits 1 when a folder does not compile. apply checks every folder
before it sends anything, so one broken folder stops the whole apply. The
example compiles, with a slot derived for each credential:
$ npx nylorun check agents/hello
agents/hello/mcp.json:7:26 info: A bearer slot ORDERS_MCP_TOKEN is derived for https://orders.example.com/mcp
agents/hello/tools/get_order.json:20:19 info: A bearer slot ORDERS_TOKEN is derived for https://orders.example.com/lookup
agents/hello/README.md note: Kept with the source; not part of the agent
agents/hello: ok, agent manifest hello (5 files, agent-folder/1)orders.example.com does not answer: replace the example tool and server
with your own, or delete them, before you talk to the Agent.
apply --agent [<id>] makes the Agent <id> from the manifest after the
save, by default named after the manifest, or moves an existing one to the
version saved, as agents put does. It does so also when the save is
unchanged, and prints what the Agent needs a person for, such as a required
secret with no value. <id> needs one folder; with several folders and no id,
each Agent is named after its manifest. Write --agent=<id> when <id> is
also a directory name.
apply --watch checks and plans again on each change to the folders, and to
the files their runtime.json extends, until interrupted. With
--save it also saves each change. Each save is a new version that moves
every Agent tracking the manifest, so use it on a development manifest. A plan
prints a change to the instructions as a text diff, in - and + lines.
chat <agent> opens a new session on the Agent, or the one --session <id>
names, which is created with that id when it does not exist, so one session
can carry across runs. It sends --message once, or each line of standard
input, and prints the reply as it comes. The session's owner is --owner,
else the Agent's owner, else --as, else $USER. A turn that waits for a
person prints how to answer it with nylorun sessions, and a turn that fails
exits 1. To send JSON input, such as a flow's, use
nylorun sessions send <id> --data <json>.
After the first save, the Agent tracks the manifest, so each later apply
moves it to the new version. Write each secret slot's value, then talk to it:
npx nylorun apply agents/triage --agent teams/triage
printf %s "$GITHUB_TOKEN" | npx nylorun agents secrets put teams/triage GITHUB_TOKEN
npx nylorun chat teams/triage --message "Triage acme/api#42"agents put teams/triage --manifest triage, sessions create, and
sessions send do the same step by step. Use agents put to make a second
Agent from the manifest, or to pin one to a version. See
Agents.
The commands act on the project's local Tenant, or on the Runtime that
NYLORUN_RUNTIME_URL and NYLORUN_SERVER_KEY name. See
CLI.
Layout
support/
├── AGENT.md required: frontmatter, and the instructions as its body
├── instructions/*.md more instructions, after the body
├── runtime.json the runtime section
├── mcp.json remote MCP servers, in Agent Plugins syntax
├── tools/<name>.json one HTTP tool per file
├── triggers/<id>.md one trigger per file; the body is its prompt
├── skills/<name>/ one Agent Skill per folder, with its SKILL.md
├── files/** files copied under /workspace on a pod's new disk
├── schemas/*.json JSON Schemas that files name by path
├── *.schema.json JSON Schemas, such as output.schema.json
├── agents/<id>/ an agent it uses as a tool, or one of a flow's agents
├── flow.json makes the folder a flow
├── plugins/<name>/ a vendored Agent Plugin
├── .nyloignore what packing leaves out
└── anything else kept with the source, not compiled (README.md, docs/)Only AGENT.md is required. The directory gives a file its kind, and the file
name its name: tools/lookup_order.json is the HTTP tool lookup_order,
triggers/nightly.md the trigger nightly, and agents/billing/ the agent
billing. Prose is Markdown with YAML frontmatter; everything structured is
JSON. Subagents, flows, and plugins are
folders of the same format inside the folder.
AGENT.md
---
name: triage
description: Reads a GitHub issue and proposes a label and a priority.
outputSchema: ./output.schema.json
mcpServers:
github:
tools: { "*": { enabled: false }, issue_read: { enabled: true } }
memory:
access: read_only
---
Triage the GitHub issue the user names as owner/repo#number. Read it, and its
comments, with the github tools.| Key | Type | Default | Manifest field |
|---|---|---|---|
name | Required. 1 to 64 letters, digits, ., _, and -, starting with a letter or digit. | name | |
description | Required. A non-empty string. At most the Tenant's manifestDescriptionChars (1024 by default), checked at save. | description | |
format | 1. | 1 | None: the compiler's format. |
harness | A harness id: a lowercase letter, then 1 to 62 lowercase letters, digits, and -. Any harness other than nylorun needs a runtime.json of kind pod with an image. See Harness. | nylorun | harness.id |
inputSchema | A flow's input: a path to a JSON Schema file, or the schema inline. A flow only; elsewhere it is input_schema_refused. See Flows. | None | harness.inputSchema |
outputSchema | A path to a JSON Schema file, or the schema inline. | None | harness.outputSchema |
mcpServers | This agent's settings for each server of mcp.json. See MCP servers. | None | harness.mcpServers.<server>.deferred, .tools |
skills | This agent's settings for each skill of skills/. See Skills. | None | harness.skills.<name>.secrets |
memory | { access, instructions }: access is read_write or read_only, instructions at most 4096 characters. See Agent memory. | Off | harness.memory |
loop | A map: the harness's own settings, passed to it with each turn. A key the nylorun harness does not read is a loop_key warning. | None | harness.loop |
metadata | A map of your own. | None | metadata |
The manifest is saved under name. Over HTTP, name must equal the name in
the path. The folder's own name does not matter.
The body is the first instruction. instructions/*.md follow, one instruction
per file, in natural order: 2-style.md before 10-tone.md. Leading and
trailing blank lines are trimmed and nothing else is changed. An empty file
adds no instruction, and an agent with no instruction at all is an
instructions_empty warning. instructions/ holds Markdown files only, with
no subfolders. See Instructions. A flow's
AGENT.md has frontmatter only: see Flows.
Frontmatter
Frontmatter sits between two --- lines at the top of the file. It is YAML
1.2 with the core schema: no anchors, aliases, tags, or merge keys, and a key
written twice is an error. Quote a value that starts with * or &, such as
a cron (schedule: "0 2 * * *"). An unknown key is an error that names the
closest known key, and check --fix renames it when exactly one is close.
Write AGENT.md and every other file as UTF-8. A byte order mark is ignored.
A file with CRLF line ends is read as LF, with a crlf note that
check --fix resolves; the .gitattributes that nylorun init writes
(* text=auto eol=lf) keeps a Windows checkout LF.
runtime.json
{
"kind": "virtual",
"network": { "allowedHosts": ["api.github.com"] }
}runtime.json is the manifest's runtime section, with the same fields:
kind, image, resources, storage, network, lifecycle, setup,
files, limits, secrets, triggers, and selfWake. See
Saving manifests. It also takes $schema
(see Editor schemas) and extends, and the
compiler leaves both out of the manifest.
Without runtime.json, an Agent made from the manifest is kind none. A
folder without one that still needs a runtime section, for a
derived slot, a trigger, or files/, compiles with
{ "kind": "none" } plus those.
extends
{
"$schema": "https://docs.nylorun.com/schemas/agent-folder/v1/runtime.json",
"extends": "../../platform/pod.runtime.json",
"resources": { "memory": "4GiB" }
}{
"kind": "pod",
"image": "python:3.13-slim",
"resources": { "cpus": 2, "memory": "2GiB" },
"network": { "allowedHosts": ["pypi.org", "files.pythonhosted.org"] }
}extends names a shared runtime file outside the folder, by a path from the
folder. nylorun check, nylorun apply, and packFolder merge it under the
folder's file: the folder's keys win, objects merge, and arrays replace. The
folder above runs on a pod with 2 CPUs and 4GiB. They send the merged
runtime.json, and record the files it came from in .nylorun/origin.json,
which is uploaded with the folder, so each saved version holds everything it
was compiled from. A missing file or a cycle is a FolderError.
The Runtime refuses an unresolved extends (not_supported: "resolve extends
before uploading"), so a client that uploads a folder itself merges it first.
mcp.json
{
"mcpServers": {
"github": {
"type": "streamable-http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": {
"Authorization": "Bearer ${GITHUB_TOKEN}",
"X-MCP-Toolsets": "issues",
"X-MCP-Readonly": "true"
}
}
}
}mcp.json holds remote MCP servers in
Agent Plugins syntax,
keyed by name. It takes mcpServers and an optional $schema: ours or
Agent Plugins' https://agent-plugins.org/schemas/1.0.0/mcp.schema.json (see
Editor schemas).
| Server key | Value |
|---|---|
type | Required. streamable-http or sse. |
url | Required. The server's URL. |
headers | A map of header names to strings. |
A server's name is 1 to 64 letters, digits, _, and -. The Runtime runs
remote servers only, so a server with no type, or of type stdio or ws,
is refused. Claude Code's "type": "http" is refused with the fix: write
"streamable-http". See MCP.
Credentials
${NAME} in a header names the secret slot the server authenticates with. It
is never read from your environment. The header leaves the manifest, and the
server gets credential: "NAME":
| Header | In the manifest | Slot |
|---|---|---|
"Authorization": "Bearer ${GITHUB_TOKEN}" | removed; credential: "GITHUB_TOKEN" | bearer, for the server's URL |
"X-Api-Key": "${ACME_KEY}" | removed; credential: "ACME_KEY" | headers, for the server's URL, described as setting X-Api-Key |
"X-MCP-Readonly": "true" | kept as written | none |
Refused:
Bearer ${NAME}in a header other thanAuthorization.${NAME}inside a longer value, other thanBearer ${NAME}.- A default,
${NAME:-dev}: a secret has none. ${…}inurl.- Two credentials on one server.
- A name outside 1 to 64 letters, digits,
.,_, and-.
A header without ${…} is saved as written, in the manifest and in the
source, where anyone with agents:read can read it. Name a token as ${NAME}
and write its value to the Agent's secrets instead. A literal value in an
Authorization, X-Api-Key, Api-Key, Cookie, or *-Token header, or a
token-shaped string (ghp_, github_pat_, sk-, xox…-, a JWT) in any
header, is a secret_literal warning. It warns and never refuses:
agents/acme/mcp.json:7:22 warning: X-Api-Key holds a literal value, not a ${NAME} slot. The folder is kept byte-exact as the version's source, readable by anyone with agents:read, and the value goes into the manifest as written. Fix: write "${NAME}" and set the value in the Agent's vaultThis agent's settings
How this agent uses a server is in AGENT.md, under mcpServers, keyed by the
server's name in mcp.json:
| Key | Value |
|---|---|
deferred | true or false defers every tool of the server, or none. See Deferred tools. |
tools | Settings per tool, keyed by the server's own tool names, "*" for the rest: enabled and deferred. See Per-tool settings. |
credential | A plugin's server only: the secret slot it authenticates with. A server in mcp.json names its credential in a header instead. See plugins/. |
A name that mcp.json does not declare is an error. Which calls wait for a
person is the Tenant's tool policy, which names an MCP
tool by its server and its own name, { "server": "github", "tool": "issue_read" }.
approval in a tool's settings is an approval_removed error (see
tools/).
tools/
{
"description": "Read one order by its id.",
"inputSchema": {
"type": "object",
"properties": { "orderId": { "type": "string" } },
"required": ["orderId"]
},
"outputSchema": "./schemas/order.json",
"http": {
"url": "https://orders.example.com/orders/{orderId}",
"method": "GET",
"credential": "ORDERS_TOKEN"
}
}Each tools/<name>.json is one HTTP tool named after its
file: 1 to 64 letters, digits, _, and -, with no __.
| Key | Type | Default |
|---|---|---|
description | What the tool does, for the model. | None |
inputSchema | Required. A JSON Schema, inline or a path to a schema file. | |
outputSchema | A JSON Schema, inline or a path. A JSON answer that does not match fails the call. | None |
http | Required. The request. |
http key | Value | Default |
|---|---|---|
url | Required. The URL, whose path may name input fields as {field}. See The request. | |
method | GET, POST, PUT, PATCH, or DELETE. | POST |
credential | A secret slot bound to url. See Secret slots. | None |
timeoutMs | Milliseconds, at most the Tenant's Runtime limit httpToolTimeoutMs (300 s by default), checked at save. | 60 s |
A tool file takes no name, since its file name is its name. It may take
$schema (see Editor schemas). tools/ holds .json
files only, with no subfolders.
approval is not a key of an agent manifest. In a tool file, or in an MCP
tool's settings in AGENT.md, it is an approval_removed error at its line:
agents/orders/tools/get_order.json:4:3 error: approval is not set in an agent folder: tool approvals are the Tenant's tool policy, decided at the Tool Gate for every agent. To have a person approve a call, add a rule with "effect": "ask" for the tool to the Tenant's tool policy (nylorun tenant tool-policy put <file>, or PUT /v1/tenant/tool-policy)Hold or refuse calls with the Tenant's tool policy,
which names an HTTP tool by its file name: { "effect": "ask", "tool": "get_order" }.
agent in a tool file is tool_invalid: an agent used as a tool is a folder,
agents/<id>/.
triggers/
---
schedule: "0 2 * * *"
timezone: Asia/Kolkata
session: new
---
Triage every issue opened in acme/api since yesterday, one line each.Each triggers/<id>.md is one trigger named after its
file: 1 to 64 letters, digits, ., _, and -, starting with a letter or
digit. The body is the prompt each run sends, and must not be empty.
| Key | Value |
|---|---|
schedule | A five-field cron, quoted. |
timezone | An IANA time zone, with schedule only. Default UTC. |
at | One run, at an RFC 3339 time with an offset, such as "2026-11-01T09:00:00+05:30". |
webhook | standard-webhooks. See Webhooks. |
session | Required. A session id of the Agent, or new for a new session per run. |
A trigger has exactly one of schedule, at, and webhook. A trigger file
needs frontmatter. One id in both triggers/ and runtime.json's triggers
is an error.
check reads a cron and a time zone as the Runtime does, in triggers/*.md
and in runtime.json's triggers: a cron that does not parse is
schedule_invalid, and an unknown time zone timezone_invalid, with a
did-you-mean. An at in the past, and a schedule that fires more often than
the Tenant allows, are refused only when saved.
skills/
skills/open-pr/
├── SKILL.md
└── scripts/open.shEach skills/<name>/ is an Agent Skill, hashed and
mounted as any skill is. Its SKILL.md frontmatter takes the Agent Skills keys
only:
| Key | Value |
|---|---|
name | Required. 1 to 64 lowercase letters, digits, and single hyphens, equal to the folder's name. |
description | Required. At most 1024 characters. |
license, compatibility, metadata, allowed-tools | Optional. Not part of the manifest. |
Which secret slots a skill's scripts read is this agent's choice, so it is in
AGENT.md, not SKILL.md:
---
name: releaser
description: Opens release pull requests.
skills:
open-pr: { secrets: [GH_TOKEN] }
---A SKILL.md with secrets is a skill_secrets error, fixable with
check --fix, which moves the list to AGENT.md's skills block when
AGENT.md lists none for the skill, or lists them all:
$ npx nylorun check agents/releaser
agents/releaser/skills/open-pr/SKILL.md:4:1 error: secrets is not an Agent Skills key: which secrets a skill gets is the agent's choice. Fix: move it to AGENT.md's skills block, as skills: { open-pr: { secrets: [...] } } (fixable with --fix)
agents/releaser: 1 errorWhen AGENT.md writes skills in flow style, such as skills: {}, --fix
leaves it and says to add the list by hand.
Each secret is an environment variable name, and must be an
environment_secret or environment_variable slot of runtime.json. Skills
need a sandbox: runtime.json of kind virtual or pod. check checks both
(manifest_inconsistent, at the field). See
Shell credentials.
files/
Each file under files/ is copied to the same path under /workspace when a
pod's disk is made or reset: files/data/orders.csv lands at
/workspace/data/orders.csv. They become one runtime.files entry,
{ "path": "/workspace", "files": { … } }, by hash, after any files that
runtime.json lists. files/ needs runtime.json of kind pod, and
network a sandbox kind; check refuses either otherwise (runtime_kind).
See Setup and input files.
Schema files
inputSchema and outputSchema in AGENT.md and in a tool file take a JSON
Schema inline or a path to a schema file. A schema file is any .json file
under schemas/, or a *.schema.json file outside the reserved directories.
It holds one JSON object.
A path names a file from the folder's root, whichever file names it:
./output.schema.json, output.schema.json, and /output.schema.json are
the same file, and a tool file names ./schemas/order.json. A path with ..
is refused. The schema is copied into the manifest; the file itself is kept
with the source.
Every schema, inline or a file, is checked as JSON Schema 2020-12 at its line
and column (json_schema_invalid):
agents/bad/AGENT.md:6:25 error: type "objekt" is not a JSON Schema type. Did you mean 'object'? The types are null, boolean, object, array, number, string and integerIt is also checked as the engine reads it each turn: if and then, not,
dependentRequired, unevaluatedProperties, and a $ref outside $defs are
refused. A keyword close to a known one, such as proprties, is a
json_schema_keyword warning that check --fix renames. A schema file's own
$schema is its dialect and stays.
Editor schemas
Every JSON file of a folder may start with $schema, for an editor; the
compiler leaves it out of the manifest. Each kind of file has a JSON Schema
2020-12, built from what the compiler checks:
| File | Schema |
|---|---|
AGENT.md frontmatter | /schemas/agent-folder/v1/agent-md.json |
triggers/<id>.md frontmatter | /schemas/agent-folder/v1/trigger-md.json |
runtime.json | /schemas/agent-folder/v1/runtime.json, or /schemas/runtime-manifest/v1.json |
tools/<name>.json | /schemas/agent-folder/v1/tool.json |
mcp.json | /schemas/agent-folder/v1/mcp.json, or Agent Plugins' mcp.schema.json |
flow.json | /schemas/agent-folder/v1/flow.json |
Each URL is on https://docs.nylorun.com, such as
https://docs.nylorun.com/schemas/agent-folder/v1/runtime.json. A Markdown
file takes no $schema: the two frontmatter schemas are for an editor that
checks YAML frontmatter. A $schema that names another kind of file, or a
schema that is not one of these, is a schema_url warning, and the file is
read by its path either way. The schemas are also in @nylorun/core, as
@nylorun/core/schemas/agent-folder-<kind>.json.
nylorun init agents/hello maps agents/**/runtime.json,
agents/**/tools/*.json, agents/**/flow.json, and agents/**/mcp.json
(outside plugins/) to their schemas in .vscode/settings.json.
Secret slots
An HTTP tool's http.credential, a ${NAME} in an mcp.json header, and a
plugin server's credential in AGENT.md name a secret slot bound to that
URL. When runtime.json does not declare the slot,
the compiler derives one and reports it as info:
{ "type": "bearer", "url": "https://api.githubcopilot.com/mcp/", "required": true }A ${NAME} alone in a header derives a headers slot instead. A slot that
runtime.json declares wins, and check checks it as the save does: a
bearer or headers slot bound to the exact URL. A slot binds one URL, path
and query included, so one credential name on two URLs, such as
tools/get_order.json and tools/refund.json, is an error naming both files:
give each URL its own slot.
Write each slot's value into the Agent:
npx nylorun agents secrets put <agent> <name>, or
client.agents.secrets.put. Until a required slot has a value, the Agent's
state is input_required, with secret_unset in its attention. See
Secret slots.
Subagents
Without flow.json, each agents/<id>/ beside AGENT.md is an agent manifest
offered to the agent as the tool <id>, with the subagent's description and
the input { task }, as .tools(child) offers an
agent used as a tool. It runs on the session's Agent, in its runtime. See
An agent used as a tool.
<id> takes letters, digits, _, and -, up to 64 characters
(agent_name), and must not also name a tools/<id>.json (tool_twice). A
subagent is an agent manifest without what belongs to the folder it is in:
| In a subagent | Refused as |
|---|---|
runtime.json, triggers/, files/ | nested_refused: they are the outer folder's. |
format, harness, memory, or loop in its AGENT.md | nested_refused: it runs in the outer agent's harness. |
agents/ of its own | subagent_depth: agents used as tools nest one level deep. |
A credential on its MCP servers, or secrets on its skills | credential_unsupported: only the outer agent's servers and skills get them. |
A subagent with its own flow.json is a flow offered as a tool, and its
stages name the agents under its own agents/.
Flows
flow.json makes the folder a flow agent
(harness.kind: "workflow"). It holds one node, the flow's root, and each
node has exactly one of these keys, and an optional id:
| Node | Runs | Builder |
|---|---|---|
{ "agent": "<id>" } | The agent agents/<id>/ of the flow root. | An agent stage |
{ "tool": "<name>" } | The HTTP tool tools/<name>.json beside this flow.json. | An HTTP stage |
{ "chain": [ … ] } | Each node in order, each one's output the next one's input. | Pipe |
{ "switch": { "cases": { … }, "default": … } } | The one case the previous stage's output names. | Switch |
{ "parallel": { "<key>": … } } | Each named branch at once, on the same input. | Parallel |
{ "map": { "each": … } } | The node once per item of the previous stage's output. | Map |
{ "loop": { "run": …, "verify": …, "max": <n> } } | run, then verify, until it passes or max runs. verify is { "agent": "<id>" } or { "tool": "<name>" }. | Loop |
Every flow.json under the flow root names its stages from the root's one
agents/, by id. An agents/<id>/ with its own flow.json is a sub-flow over
the same agents, so it has no agents/ of its own (flow_namespace), and a
sub-flow that runs itself is flow_cycle. An id with no folder is
agent_unknown, and a tool with no file tool_unknown, each with a
did-you-mean. A tool file no node names is tool_unused: a flow holds only
the tools its nodes name.
The flow's AGENT.md has frontmatter only (flow_body), and may set
inputSchema, the flow's input. A flow has no instructions/, mcp.json,
skills/, or plugins/, and its AGENT.md no mcpServers, skills, or
memory (flow_refused): each of its agents has its own. The flow root keeps
runtime.json, triggers/, and files/, and its agents run in that runtime.
An agent of agents/ that no flow.json names is an agent_unused warning,
and is left out of the manifest. See A flow.
Plugins
plugins/<name>/ holds an Agent Plugin as its author
published it. The compiler folds it in as .plugin() does: its skills and its
mcp.json servers join the agent's, each labelled with the plugin's name and
version, and harness.plugins lists the plugin.
pr-bot/
├── AGENT.md
├── runtime.json
└── plugins/github/
├── plugin.json the Agent Plugins $schema, name, version
├── mcp.json the Agent Plugins MCP $schema, and its servers
├── skills/open-pr/SKILL.md
└── hooks/hooks.json not run: a plugin_part_ignored note{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "github",
"version": "1.2.0",
"description": "GitHub's MCP server and a skill that opens pull requests."
}$ npx nylorun check pr-bot
pr-bot/plugins/github/mcp.json:7:37 info: A bearer slot GITHUB_TOKEN is derived for https://api.githubcopilot.com/mcp/
pr-bot/plugins/github/hooks/ note: Hooks are not run by Nylorun
pr-bot: ok, agent manifest pr-bot (6 files, agent-folder/1)A plugin needs plugin.json with the Agent Plugins $schema and a name,
and its mcp.json the Agent Plugins MCP $schema (plugin_invalid). An
invalid skill or server is an error, as in the agent's own skills/ and
mcp.json. A skill or server whose name the agent already has is
plugin_duplicate. What Nylorun does not run, such as hooks, commands, and
Claude Code's agents/*.md, is kept with the source with a
plugin_part_ignored note, and a plugin.json field Agent Plugins 1.0.0 does
not have is a plugin_unknown_key warning. A flow takes no plugins: vendor a
plugin in the agent that uses it.
.nyloignore
notes/
*.log.nyloignore in a folder names, in gitignore syntax, the files that packing
leaves out: nylorun check, nylorun apply, and packFolder neither compile
nor upload them. Packing always skips .git, .hg, .svn, and
node_modules, and follows no symbolic link. .nyloignore itself is kept with
the source.
How a folder maps to the manifest
| Folder | Agent manifest v8 |
|---|---|
AGENT.md name, description, metadata | name, description, metadata |
AGENT.md harness | harness.id |
AGENT.md body, then instructions/*.md | harness.instructions, one string per file |
AGENT.md outputSchema, memory, loop | harness.outputSchema, harness.memory, harness.loop |
AGENT.md inputSchema (a flow) | harness.inputSchema |
mcp.json, with AGENT.md mcpServers | harness.mcpServers, credential from ${NAME} |
skills/<name>/, with AGENT.md skills | harness.skills.<name>, with files by hash and secrets |
tools/<name>.json | harness.tools[], an HTTP tool named <name> |
agents/<id>/, with no flow.json | harness.tools[], an agent used as a tool named <id> |
flow.json, with agents/ | harness.kind: "workflow", harness.root, and harness.agents |
plugins/<name>/ | Its skills and MCP servers, labelled with the plugin, and harness.plugins |
runtime.json, after extends | runtime |
| A derived slot | runtime.secrets.<name> |
triggers/<id>.md | runtime.triggers.<id>, with the body as prompt |
files/** | runtime.files[], { path: "/workspace", files } |
| Anything else | Nothing; kept with the source |
nylorun apply <dir> --dry-run prints the compiled manifest, and
nylorun check <dir> --json prints it offline. The same folder always
compiles to the same manifest, whatever order its files are listed in.
Examples
An agent
writer/
├── AGENT.md
├── instructions/01-style.md
├── output.schema.json
└── README.md---
name: writer
description: Writes a one-paragraph release note from a list of changes.
outputSchema: ./output.schema.json
metadata:
team: docs
---
Write one paragraph that a customer can read, from the changes the user lists.
Leave out internal names.Use the present tense. No marketing adjectives.{
"type": "object",
"properties": { "title": { "type": "string" }, "note": { "type": "string" } },
"required": ["title", "note"]
}It compiles to a manifest with no runtime section, so its Agents are kind
none. README.md is kept with the source.
An HTTP tool and its secret
orders/
├── AGENT.md
├── schemas/order.json
└── tools/get_order.json---
name: orders
description: Answers questions about one order.
---
Look the order up with get_order before you answer. Never guess a status.tools/get_order.json is the tool above, and schemas/order.json
its output schema. Its credential derives a bearer slot ORDERS_TOKEN
bound to https://orders.example.com/orders/{orderId}, with a runtime section
of kind none:
npx nylorun apply orders --agent teams/orders
printf %s "$ORDERS_TOKEN" | npx nylorun agents secrets put teams/orders ORDERS_TOKENThe Runtime sends the Agent's ORDERS_TOKEN as Authorization: Bearer … on
each call. The tool's name in a tool policy rule is get_order.
An MCP server
triage/
├── AGENT.md
├── mcp.json
└── runtime.json---
name: triage
description: Reads a GitHub issue and proposes a label and a priority.
mcpServers:
github:
deferred: false
tools: { "*": { enabled: false }, issue_read: { enabled: true } }
---
Triage the GitHub issue the user names as owner/repo#number. Read it, and its
comments, with the github tools.
Answer with one label (bug, feature, question or docs), a priority from p0
(urgent) to p3, and one sentence on why. Change nothing on GitHub.mcp.json is the file above. runtime.json declares the slot
itself, with a description, so nothing is derived:
{
"kind": "virtual",
"network": { "allowedHosts": [] },
"secrets": {
"GITHUB_TOKEN": {
"type": "bearer",
"url": "https://api.githubcopilot.com/mcp/",
"required": true,
"description": "A GitHub token that may read issues, for GitHub's MCP server"
}
}
}A smaller version, with no runtime.json, so that its slot is derived, is in
the nylorun/agents repository as
examples/agents/triage.
Triggers
digest/
├── AGENT.md
├── mcp.json
├── triggers/nightly.md
└── triggers/alerts.md---
schedule: "30 8 * * 1-5"
timezone: Asia/Kolkata
session: digest
---
Summarize the issues opened in acme/api since the last digest, one line each.---
webhook: standard-webhooks
session: new
---
An alert arrived. Read it, find the issue it concerns, and say what to do.nightly runs on weekdays at 08:30 in Kolkata, always in the session
digest. alerts runs once per signed delivery, each in a new session. Each
Agent made from the manifest has its own copy of both. Make the webhook
trigger's signing secret, shown once with its URL, with
npx nylorun agents triggers new-secret <agent> alerts. See
Triggers and wakes.
A flow
brief/
├── AGENT.md
├── flow.json
└── agents/
├── researcher/AGENT.md
└── writer/AGENT.md---
name: brief
description: Researches a topic, then writes a short brief on it.
---{ "chain": [{ "agent": "researcher" }, { "agent": "writer" }] }---
name: researcher
description: Lists what matters about the topic.
---
List the five facts about the topic a newcomer most needs, each with one sentence of context.---
name: writer
description: Writes the brief.
---
Write a brief of at most 200 words from the facts you are given, for a reader new to the topic.$ npx nylorun check brief
brief: ok, agent manifest brief (4 files, agent-folder/1)The flow sends its input to researcher, then researcher's answer to
writer, whose answer is the flow's. A flow that uses every node is
examples/agents/repo-brief-folder
in the nylorun/agents repository: a switch, a map, a parallel stage, a loop,
and an HTTP tool stage, in a sub-flow:
{
"chain": [
{ "agent": "triage" },
{ "id": "route", "switch": {
"cases": { "repo": { "agent": "research" } },
"default": { "agent": "decline" } } }
]
}{
"chain": [
{ "id": "ask", "map": { "each": { "agent": "researcher" } } },
{ "id": "review", "parallel": { "overview": { "agent": "overview" }, "risks": { "agent": "risks" } } },
{ "id": "draft", "loop": { "run": { "agent": "writer" }, "verify": { "agent": "editor" }, "max": 2 } },
{ "tool": "publish_brief" }
]
}$ npx nylorun check repo-brief-folder
repo-brief-folder: ok, agent manifest repo-brief (23 files, agent-folder/1)An agent used as a tool
support/
├── AGENT.md
└── agents/billing/AGENT.md---
name: support
description: Answers customer questions, and hands billing questions to billing.
---
Answer the customer's question. For a question about an invoice or a refund,
call billing with the question as the task, and pass on its answer.---
name: billing
description: Answers one question about invoices and refunds.
---
Answer the billing question in the task in two sentences or fewer.The model calls billing with { "task": "…" }, and gets billing's answer
as the tool's result. The manifest embeds billing in the tool, so one save
holds both.
Diagnostics
Each problem comes back as file:line:column severity: message, every
problem the compiler finds in one run:
$ npx nylorun check agents/bad
agents/bad/AGENT.md:4:1 error: Unknown key 'skils'. Did you mean 'skills'? (fixable with --fix)
agents/bad/AGENT.md:6:25 error: type "objekt" is not a JSON Schema type. Did you mean 'object'? The types are null, boolean, object, array, number, string and integer
agents/bad/AGENT.md note: The file has CRLF line ends, read as LF. Fix: save it with LF line ends (fixable with --fix)
agents/bad: 2 errorsSome errors stop the checks that depend on them, such as a SKILL.md with
secrets, an invalid schema, or a trigger that does not parse, so check again
after you fix them. A problem one edit fixes ends (fixable with --fix).
| Severity | Means |
|---|---|
error | The folder does not compile, and nothing is sent. |
warning | The folder compiles. Read it before you save. See Warnings. |
info | Something the compiler added, such as a derived slot (slot_derived). |
note | A file kept with the source and not compiled (file_not_compiled), a part of a plugin Nylorun does not run, or CRLF line ends read as LF. |
Messages name the field, what it takes, and the closest value:
agents/typo/runtime.json:1:11 error: kind is "virtual", "pod" or "none", not "virtal". Did you mean 'virtual'?The Runtime answers a folder that does not compile with
422 folder_invalid, every diagnostic in details.diagnostics with its
code, file, line, column, the manifest pointer when there is one,
and its fix when it has one.
Refused by the compiler
| Code | When |
|---|---|
agent_missing, frontmatter_missing | No AGENT.md at the root, or one without frontmatter. |
name_missing, name_invalid, name_mismatch, description_missing | name or description missing or invalid, or name other than the name saved under. |
unknown_key | A key the file does not take, with "Did you mean" or the known keys. |
value_invalid, harness_invalid | A frontmatter value of the wrong type, such as metadata that is not a map, or a bad harness id. |
json_invalid, frontmatter_invalid, not_utf8 | A file that does not parse: JSON comments, trailing commas, a key twice, YAML aliases or tags. |
wrong_name, wrong_syntax | agent.md in lower case, or runtime.yaml, mcp.yaml, runtime.jsonc, and the like. |
reserved_directory | A file tools/, triggers/, instructions/, skills/, or schemas/ does not take, such as tools/notes.txt. |
path_invalid, path_case, path_duplicate, path_refused | A path with .., \, control characters, or not in NFC; two paths that differ only in case; .git/ or node_modules/. |
file_missing | A file the folder lists whose bytes the compiler was not given. |
mcp_type, mcp_invalid | A server with no type, stdio, ws, or http, or with no url. |
credential_placement, credential_default, credential_name, credential_twice, credential_urls | A ${NAME} the compiler cannot bind to a slot. See Credentials. |
server_unknown, skill_unknown | AGENT.md names a server or skill the folder does not have. |
skill_invalid, skill_secrets | A skill without SKILL.md, with a bad name or description, or with secrets. |
tool_invalid | A tool file with name or agent, without http or inputSchema, or with a bad file name. |
approval_removed | approval in a tool file or an MCP tool's settings. See tools/. |
schema_missing, schema_invalid, path_outside | A schema path to no file, to a file that is not a schema file, or outside the folder; or a field that breaks the manifest's schema. |
json_schema_invalid | A schema that is not valid JSON Schema 2020-12, or uses what the engine does not read. See Schema files. |
schedule_invalid, timezone_invalid | A cron that does not parse, or an unknown time zone. |
trigger_twice, prompt_empty | A trigger in both triggers/ and runtime.json, or a trigger with an empty body. |
manifest_inconsistent | Sections that do not fit, at the field: skills with no sandbox, a custom harness off a pod, a credential bound to another URL. |
runtime_kind | files/ off a pod, or network on kind none. |
flow_invalid, flow_body, flow_refused, flow_namespace, flow_cycle | A flow.json node that is not valid, a flow's AGENT.md with a body, what a flow does not take, agents/ in a sub-flow, or a sub-flow that runs itself. See Flows. |
agent_unknown, agent_name, tool_unknown, tool_unused, tool_twice, input_schema_refused | A stage that names no agent or tool, an agents/<id>/ with a bad id, a flow's tool no node names, an id that is both an agent and a tool, or inputSchema on an agent that is not a flow. |
nested_refused, subagent_depth, credential_unsupported | What a subagent or a flow's agent does not take. See Subagents. |
plugin_invalid, plugin_duplicate | A plugin without a valid plugin.json or mcp.json $schema, or a skill or server name the agent already has. See Plugins. |
format_unsupported | A format other than 1. |
not_supported | An unresolved extends: resolve it before uploading. See extends. |
limit_exceeded | Over a limit. |
Warnings
A warning never stops a save.
| Code | When |
|---|---|
secret_file | .env or .env.* anywhere, or a .pem or .key file outside skills/ and files/. |
secret_literal | A literal value in an authentication header, or a token-shaped string in any header. See Credentials. |
tool_policy_file | A root tool-policy.json, policy.json, or *.policy.json. Tool policy is the Tenant's; the file is only kept with the source. |
schema_url | A $schema that names another kind of file, or a schema that is not ours. See Editor schemas. |
json_schema_keyword | A JSON Schema keyword close to a known one, such as proprties. |
loop_key | A loop key the nylorun harness does not read. |
instructions_empty | An agent with no instructions. |
agent_unused | An agent of a flow's agents/ that no flow.json names. It is left out of the manifest. |
plugin_unknown_key | A plugin.json field Agent Plugins 1.0.0 does not have. |
Each secret warning says that the folder is kept byte for byte as the
version's source, readable by anyone with agents:read, and where a secret
goes instead: a ${NAME} slot, with its value in the Agent's secrets.
$ npx nylorun check agents/warn
agents/warn/.env warning: An environment file usually holds secrets. The folder is kept byte-exact as the version's source, readable by anyone with agents:read. A secret usually goes in a ${NAME} slot (in runtime.json, or a header in mcp.json), with its value set in the Agent's vault: leave this file out of the upload
agents/warn/AGENT.md:5:3 warning: Unknown loop setting 'maxSteps'. loop holds the harness's own settings, and the nylorun harness reads none yet, so it is ignored
agents/warn/AGENT.md:7:1 warning: The agent has no instructions: AGENT.md's body is empty and instructions/ holds none, so the model gets the harness's own instructions only. Write them below the frontmatter
agents/warn/tool-policy.json warning: This looks like a tool policy, and it is only kept with the source: tool policy is the Tenant's, for every agent, set with nylorun tenant tool-policy put <file> (PUT /v1/tenant/tool-policy)
agents/warn: ok, agent manifest warn (3 files, agent-folder/1)Leave a secret file out of the upload with .nyloignore.
What only the save checks
check runs every rule that needs no Runtime, so a folder that passes it
fails a save only on what depends on the Tenant:
- The Tenant's Runtime limits: the description's length,
the secret slots, a tool's
timeoutMsagainsthttpToolTimeoutMs, the hostsnetwork.allowedHostsmay name, and a trigger's interval, count, and prompt length. - An
attrigger in the past. - Files the Runtime does not hold (
definition_files_missing). - Folder limits the Tenant set lower than the defaults (see Limits).
Run npx nylorun apply <dir> --dry-run to see them before a save. The plan
names each by its manifest path. See
Sections that fit.
Fix
check --fix makes three kinds of edit, and only those:
- Renames an unknown key that has exactly one did-you-mean, where it is, and
keeps every other byte. This covers
AGENT.md, a trigger, aSKILL.md, a tool file,runtime.json,mcp.json,flow.json, and a JSON Schema's keywords. - Makes CRLF line ends LF.
- Moves
secretsfrom aSKILL.mdtoAGENT.md'sskillsblock.
It prints each change by file, then checks again:
$ npx nylorun check --fix agents/bad
fixed agents/bad/AGENT.md: CRLF line ends made LF
fixed agents/bad/AGENT.md: renamed 'skils' to 'skills' at line 4
agents/bad/AGENT.md:6:25 error: type "objekt" is not a JSON Schema type. Did you mean 'object'? The types are null, boolean, object, array, number, string and integer
agents/bad: 1 error$ npx nylorun check --fix agents/releaser
fixed agents/releaser/skills/open-pr/SKILL.md: secrets moved to AGENT.md's skills block
fixed agents/releaser/AGENT.md: skills.open-pr.secrets: GH_TOKEN, from skills/open-pr/SKILL.md
agents/releaser: ok, agent manifest releaser (3 files, agent-folder/1)Without --fix, check writes nothing. --fix leaves a file whose bytes
changed since it was checked, and a runtime.json that extends another. In
code, applyFolderFixes in @nylorun/core/folder makes the same edits on
text.
In CI
check --json prints one JSON document, with the same exit codes:
$ npx nylorun check agents/releaser --json
{
"folders": [
{
"path": "agents/releaser",
"ok": false,
"diagnostics": [
{
"severity": "error",
"code": "skill_secrets",
"message": "secrets is not an Agent Skills key: which secrets a skill gets is the agent's choice. Fix: move it to AGENT.md's skills block, as skills: { open-pr: { secrets: [...] } }",
"file": "skills/open-pr/SKILL.md",
"line": 4,
"column": 1,
"fix": {
"kind": "move_skill_secrets",
"file": "skills/open-pr/SKILL.md",
"skill": "open-pr",
"secrets": [
"GH_TOKEN"
]
}
}
]
}
]
}Each folder has its path, ok, the manifest it compiles to when it does,
and its diagnostics, each fixable one with its fix: rename_key, crlf,
or move_skill_secrets. With --fix, each folder also lists what was
fixed, and what was unfixed with why.
Limits
| Limit | Default | Runtime limit |
|---|---|---|
| Files in a folder | 2,000 | folderFiles |
| Bytes in a folder | 50 MiB | folderBytes |
| One Markdown or JSON file the compiler reads | 1 MiB | folderTextFileBytes |
| A path | 1 to 1024 characters | Fixed |
The first three are the Tenant's Runtime limits,
set with admin.settings.limits.put, nylorun tenant settings limits put, or
Studio's Limits tab. A folder over folderFiles or folderBytes is
413 limit_exceeded, with details.limit naming the limit, and a file over
folderTextFileBytes is 422 folder_invalid. nylorun check,
nylorun apply, and packFolder hold a folder to the defaults whatever the
Tenant sets, so a raised limit helps only a client that packs the folder
itself.
Each file is also uploaded as a definition file, within the Tenant's limits. A skill's files count toward the skill limit, as any skill's do.
The source
Each version saved from a folder keeps the folder as its source: every file,
byte for byte, README.md included. A version's source gives its
compiler, files, and bytes; a version saved as JSON has none. Reading it
takes agents:read.
A save whose compiled manifest equals the latest version saves nothing, as for
any manifest: it answers unchanged. When only files outside the agent
changed, such as README.md, it also notes source_not_saved: the new source
is not kept, and tracking Agents do not move.
| Call | Returns |
|---|---|
client.agentManifests.source(name, version) | GET /v1/agent-manifests/{name}/versions/{v}/source: { compiler, files, bytes, entries }, each entry { path, size, sha256 }. |
client.agentManifests.sourceFile(name, version, path) | GET …/versions/{v}/source/files/{path}: one file's bytes. |
client.agentManifests.sourceDiff(name, from, to) | GET …/source/diff?from=&to=: what changed between two versions' folders. |
Studio shows a version's folder in the manifest's Source view, each file marked when it was added or changed since the version before, and saves a folder from the browser with Upload folder…, after showing its plan. See Studio.
Compare versions
const diff = await client.agentManifests.sourceDiff("triage", 2, 3);
for (const file of diff.added) console.log("+", file.path);
for (const file of diff.removed) console.log("-", file.path);
for (const file of diff.changed) console.log("~", file.path, file.from.size, "->", file.to.size);
console.log(diff.unchanged, "files unchanged");added and removed list the files only to or only from holds,
changed the files both hold with other bytes, by SHA-256, each as from
held it and as to does, and unchanged counts the rest. Each list is
sorted by path. Either version saved as JSON is 404, and a from or to
that is not a version number 400.
Convert
| Command | Writes |
|---|---|
nylorun pull <name>[@<v>] [--out <dir>] | The folder a version was saved from, byte for byte. A version saved as JSON has none: it says to use export --folder. |
nylorun export --folder <name>[@<v>] [--out <dir>] | The folder of a saved manifest, for a version saved as JSON or from TypeScript. |
nylorun migrate folder <dir> [--out <dir>] | The folder of a three-file folder, with its skills. Reads no Runtime. |
Each writes to --out, by default ./<name>, and never writes over a file
with other bytes. pull and export default to the latest version.
export --folder and migrate folder say whether the folder compiles back to
the same manifest, and note what they write differently. A saved manifest
names a skill's files by hash only, so export leaves them for you to copy
in; migrate folder reads them from the three-file folder, and moves each
SKILL.md's secrets to AGENT.md.
$ npx nylorun migrate folder agents/triage --out agents/triage-folder
Wrote 3 files to agents/triage-folder.
It compiles back to the same manifest. Check it with nylorun check agents/triage-folder, and save it with nylorun apply agents/triage-folder.In code, folderOfManifest(manifest) from @nylorun/agents is the same
converter:
import { createClient, folderOfManifest } from "@nylorun/agents";
const client = await createClient();
const latest = await client.agentManifests.get("triage");
const { files, exact, notes } = folderOfManifest(latest.manifest);
// files: each path with its text or bytes; exact: compiles back to the same manifest
for (const note of notes) console.log(note);Save from code
import { checkFolder, createClient, diagnosticLine, packFolder } from "@nylorun/agents";
const folder = await packFolder("agents/triage");
// Offline, as nylorun check does.
const checked = await checkFolder(folder);
for (const diagnostic of checked.diagnostics) console.log(diagnosticLine(diagnostic));
if (!checked.manifest) process.exit(1);
const client = await createClient();
const plan = await client.agentManifests.planFolder(folder);
if (plan.errors.length > 0) throw new Error(plan.errors.join("\n"));
const saved = await client.agentManifests.saveFolder(folder, { ifMatch: plan.from ?? 0 });
await client.agents.put("teams/triage", { manifest: saved.manifest.name });| Call | Does |
|---|---|
packFolder(dir) | Lists the folder's files with their sizes and SHA-256. Leaves out what .nyloignore names, skips .git, .hg, .svn, and node_modules, follows no symbolic link (skipped), and resolves extends. Node only. |
checkFolder(folder) | Compiles it offline: { manifest?, diagnostics, compiler }. |
client.agentManifests.planFolder(folder, { ifMatch? }) | The plan, plus the compiled manifest, the diagnostics, and the compiler. |
client.agentManifests.saveFolder(folder, { ifMatch? }) | Saves it, uploading the files the Runtime lacks. |
diagnosticLine(diagnostic) | A diagnostic as file:line:column severity: message. |
folderOfManifest(manifest) | The folder of a manifest: { files, exact, notes }. See Convert. |
ignoreMatcher(text) | A matcher for .nyloignore text: (path, directory?) => boolean. |
planFolder and saveFolder compile the folder first, and throw
FolderInvalidError, with every diagnostic, before they send anything.
Any other client sends the folder by hash: each file with
PUT /v1/files/sha256:<hex>, then PUT /v1/agent-manifests/{name} with
Content-Type: application/vnd.nylorun.folder+json and the body
{ "format": "nylorun.folder.v2", "entries": [{ "path": "AGENT.md", "size": 182, "sha256": "<hex>" }] },
every file listed.
?dryRun=true and If-Match work as for a JSON body. A file the Runtime does
not hold is 400 definition_files_missing. The compiler is
compileAgentFolder in @nylorun/core/folder; such a client leaves out what
.nyloignore names and resolves extends itself.
Three files
agent.manifest.json, with its harness.manifest.json and
runtime.manifest.json, is the three-file format, and it is deprecated.
nylorun apply and composeManifest() still read it, and warn:
triage: warning: agent.manifest.json (the three-file format) is deprecated: nylorun migrate folder agents/triage writes it as an agent folder (AGENT.md), which saves the same manifest. It is still read until a later release.nylorun migrate folder rewrites it as AGENT.md and its files: see
Convert. The format itself is on
Saving manifests.