agents.yaml
The file a project writes, the file uze derives from it, and what each one is allowed to say
Two files at the project root, both committed, with one rule between them:
| File | Written by | Holds |
|---|---|---|
agents.yaml | you | What the project declares: which marketplaces it draws from, which plugins it wants, and, under workspace:, how its agents work |
agents.lock | uze | What resolving those declarations produced: the commit each marketplace was read at, and a digest of the bytes that landed |
The lock carries no intent, so deleting it loses nothing: uze install
regenerates it. Deleting agents.yaml loses the project's decisions.
git clone && uze install reconstructs the environment from the pair alone.
agents.yaml
Two kinds of key, one per module. The package manager's sit at the root; the
workspace's live under one workspace: section, which a project that only
installs plugins never writes:
marketplaces:
ai:
git: https://github.com/you/plugins
ref: main
plugins:
- review
- changelog
workspace:
worktree: always
delivery: prCreated by uze install or uze <plugin>@<market>, explicit acts of setting a
project up, with the package manager's vocabulary written out and commented.
Nothing about the workspace is written, not even commented: the section appears
when you choose something in the workspace or write it yourself. Nothing that
merely reads a project creates the file.
Every key is optional; a project declaring only a workspace policy is as valid
as one declaring only plugins. Unknown keys are an error, not silence: a
typo in a file a person wrote should not be read as a decision. Writing
revision: here is refused by name: that belongs to the lock.
This document is patched in place, so a comment you wrote beside an unrelated
entry survives uze <plugin>@<market>.
marketplaces
Every marketplace the project draws from, keyed by the name you refer to it by, with what it takes from each. Exactly one source per marketplace:
marketplaces:
ours:
git: https://github.com/acme/plugins
ref: main # optional: branch, tag or sha; also the pin that moves
subdirectory: market # optional
plugins: [review, changelog]
local:
path: ../marketplace
plugins: [bench-runner]Plugins are bare names: the source above says where they come from, and ref:
is what moves them.
A marketplace is a Git repository, whether you spell it as a URL or as a
directory on this machine. Not because Git is how the bytes travel (a
directory does that), but because a commit is the only thing that answers are
these the bytes I installed? and is there something newer? A path: is
therefore where this machine reads the repository; what the lock records
is its remote and a commit, which is what another machine can reproduce.
The lock records every Git source in one spelling: git@github.com:acme/plugins.git,
ssh://git@github.com/acme/plugins and https://github.com/acme/plugins.git all
become https://github.com/acme/plugins. That is a name, not a transport:
each machine reaches it anonymously when it is public, and with its own
credential helper or SSH key when it is private, so nothing in these files
says how to authenticate, and a collaborator with no key installs a public
marketplace as easily as you do. A marketplace you develop is read from your
checkout with uze market link, which touches neither file.
The official marketplace is not declared here. uze-official is built into the
binary, its plugins are installed for every project, and an entry claiming that
name is refused: the only thing it could express is shadowing the built-in one.
workspace
How uze workspace runs the agents it launches, and
where the project keeps the artifacts that describe it. One flat section. The
layout of the worktrees themselves (.worktrees/<id>, branch agent/<id>) is
fixed infrastructure and is deliberately not declared here.
The section is the workspace's. The package manager carries it without reading
it, so a mistake in it never fails uze install or uze status; the workspace
reports it.
| Key | Default | Meaning |
|---|---|---|
worktree | manual | When an agent launched here gets a worktree: always, at launch, or manual, only when you move it with To worktree. Until then it works in the primary checkout, on your branch |
delivery | handoff | What happens to finished work: handoff (leave the branch), merge (fast-forward the target), pr (push and open a request) |
target | the branch the primary checkout is on when the task is created | The branch finished work targets |
branch | agent | The vocabulary an agent's own name for its work is judged against: a preset (conventional, gitflow, flat, agent) or your own list of types. agent names no work |
link | none | Ignored files a fresh worktree links from the primary checkout, like .env |
setup | none | Prepares a worktree, run in it after linking. A failure warns; it never blocks a launch |
gate | none | Run in the task's worktree on the rebased commits. A non-zero exit refuses delivery |
slots | peak concurrency | The most worktrees that may exist at once |
spare | 2 | Free worktrees kept warm for the next agents, the most recently used first. Every other free one is removed, its branch kept |
idle_days | 3 | Days a free worktree may sit unused before it is removed too |
artifacts | none | Where the project keeps what describes it; see below |
Every key but artifacts is about a worktree, so none of them does anything for
an agent working in the primary checkout until it is moved into one.
setup and gate each take one command or a list run in order. Use the list
when a failure should say which step failed:
workspace:
worktree: always
delivery: pr
target: main
branch: conventional
link: [.env, .env.local]
setup: pnpm install
gate:
- pnpm test
- pnpm lint
slots: 3
spare: 2
idle_days: 3
artifacts: docsA command is a line for a POSIX shell, the sh Linux and macOS run it in.
For a project worked on from Windows too, spell a step for each shell instead:
Windows runs its windows spelling in Windows PowerShell. A command both
shells read the same way is still written twice, because each shell is only
ever handed the line written for it:
workspace:
setup:
posix: cp .env.example .env
windows: Copy-Item .env.example .env
gate:
- posix: pnpm test
windows: pnpm test
- posix: test -f dist/app.js
windows: if (-not (Test-Path dist/app.js)) { exit 1 }A step with no spelling for the machine it runs on is not run there. A setup
step is skipped with a warning naming it; a gate step refuses the delivery,
because work nothing checked never lands. uze status lists every such step.
A link entry must be relative, stay inside the repository, and be ignored
by it: a tracked file linked into a worktree would land in the agent's
commits as a symlink. All three are checked when the file is read, not
silently dropped. The agent writes through a link into your primary
checkout, so only what it reads belongs there.
The policy lives here rather than in machine state because
uze install projects it into the tracked AGENTS.md: anything
resolved outside the project would rewrite a shared file to whichever machine
ran the command. A worktree's own manifest is ignored for the same reason:
the primary checkout owns the policy.
artifacts
Where the project keeps the artifacts that describe it: one directory, or a list of them, each read as deep as it goes.
workspace:
artifacts: docs
# or several:
# artifacts: [docs, design]It names places, never kinds. What a file is, it says by its own content, and each surface takes the files it recognizes and ignores the rest: the architect surface draws the Mermaid files it finds there, and the spec surface lists the decision records. A kind of artifact uze learns later needs no new key.
An entry that climbs out of the project (../) or starts at the machine's root
refuses the whole declaration, naming the entry, because this file is checked in
and would point somewhere else on every machine.
Nothing lists the files. Each one says what it is in its own first word, which is also the area it appears under, and names itself with Mermaid's own front matter, falling back to its file name:
---
title: System context
---
C4Context
Person(dev, "Developer")
System(uze, "uze")
Rel(dev, uze, "Installs, launches, reviews")| The diagram starts with | Area |
|---|---|
C4Context, C4Container, C4Component, … | C4 |
sequenceDiagram | Sequence |
flowchart, graph | Flowchart |
Levels, and the way down to the code
C4 is a model of levels (a system, the containers inside it, the components inside one of those), and the files are joined into levels the same way they are sorted into areas: by what they already say. A box has a level below it when another C4 file draws its inside, which is a boundary carrying the box's alias:
system-context.mmd System(uze, "uze", …)
containers.mmd System_Boundary(uze, "uze") { … Container(core, "Core", …) … }
core-components.mmd Container_Boundary(core, "Core") { … }A box like that is marked, and entering it opens the file that draws its inside;
the way in is kept as a trail, every step of which goes back. The last level is
the code itself. Mermaid's own $link on an element names where it lives,
relative to the project, and entering that box opens it in the code surface:
Component(store, "store", "Rust module", "Owns installed bytes", $link="crates/core/src/store.rs")A flowchart says the same with click store href "crates/core/src/store.rs".
Within an area the files are listed by name, except C4, which is listed the way it is read: the context, then containers, then components, then the dynamic and deployment views.
A .mmd or .mermaid file of any other kind is still listed, under Other,
with the reason it was not drawn: a file that silently fails to appear looks
like a lost file.
Everything here is a file somebody wrote. What the project is (where its
lines are and which of them are hot) is measured rather than written, and is
the code surface's map: press m beside the file tree, or its Map chip.
agents.lock
Generated, never edited, and deleted outright when nothing is left to
reproduce. It is spelled the way agents.yaml spells things, so a reader
moving between the two files does not learn the same vocabulary twice:
version: 1
marketplaces:
ai:
git: git@github.com:you/ai.git
revision: 02d2c9831eb619821c574622a78bbc5f6189485a
plugins:
git:
marketplace: ai
integrity: sha256:f429f157f7cec03d702f1749416ea59174b9af79c94fc281756467b0cb6b81cdResolution adds exactly two things to a declaration:
revision: the immutable commit the marketplace was read at. Non-optional besidegit, so "a marketplace with no pin" cannot be written down. Reproduction reads this, never the declaredref:: a lock that re-resolvedmainwould install whatever had been pushed since.integrity: SHA-256 over the bytes the Store ingested, verified on install before ingest and before any harness sees anything. A mismatch names both digests and installs nothing. Apath:marketplace records none: a digest of a directory somebody is editing would be wrong by the next command, and a pin that is routinely wrong teaches people to ignore pins.
Everything else in the file was already in the manifest, and is repeated only
so the lock stands alone on a machine that has never read one. version stays
at 1 while uze is pre-release; the number exists to tell apart shapes a
released uze wrote.
Staleness is decidable offline
Comparing the two files answers whether the lock still stands: no network, no
re-resolution, so uze status works in a tunnel. A declaration is stale when
the lock never resolved it, resolved it from a different marketplace, or the
marketplace it comes from is declared differently now. A lock entry the manifest
no longer declares is not staleness, it is a removal.
A floor, not a ceiling
A machine runs one revision of each plugin, shared by every project and every agent on it, and keeps it at the newest. The lock does not say what runs; it says what a machine starts from:
- A machine without the plugin installs exactly the locked revision. This is the fresh clone, the teammate, the CI runner.
- A machine behind the lock is raised to it by
uze install. A teammate moved the pin and you pulled it. - A machine past the lock keeps what it has.
uze installnever downgrades and never moves a pin, anduze statusshows the plugin as installed at a revision other than the lock's. That is the ordinary state after an update, not a fault;uze updatein the project records it.
What these files are not
They are project scope. Neither records what is installed on your machine:
that lives in the Store under ~/.uze, and uze install -m never writes
here. The two are independent by design; see
Machine and project.