@zenera/cli 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -0
  3. package/dist/args.d.ts +40 -0
  4. package/dist/args.js +99 -0
  5. package/dist/audit.d.ts +53 -0
  6. package/dist/audit.js +144 -0
  7. package/dist/banner.d.ts +13 -0
  8. package/dist/banner.js +103 -0
  9. package/dist/command.d.ts +14 -0
  10. package/dist/command.js +12 -0
  11. package/dist/commands/check.d.ts +3 -0
  12. package/dist/commands/check.js +287 -0
  13. package/dist/commands/index.d.ts +22 -0
  14. package/dist/commands/index.js +56 -0
  15. package/dist/commands/init.d.ts +3 -0
  16. package/dist/commands/init.js +157 -0
  17. package/dist/commands/inspect.d.ts +3 -0
  18. package/dist/commands/inspect.js +158 -0
  19. package/dist/commands/key.d.ts +3 -0
  20. package/dist/commands/key.js +335 -0
  21. package/dist/commands/list.d.ts +3 -0
  22. package/dist/commands/list.js +101 -0
  23. package/dist/commands/models.d.ts +9 -0
  24. package/dist/commands/models.js +120 -0
  25. package/dist/commands/open.d.ts +9 -0
  26. package/dist/commands/open.js +270 -0
  27. package/dist/commands/run.d.ts +3 -0
  28. package/dist/commands/run.js +167 -0
  29. package/dist/commands/sandbox.d.ts +3 -0
  30. package/dist/commands/sandbox.js +112 -0
  31. package/dist/commands/version.d.ts +6 -0
  32. package/dist/commands/version.js +39 -0
  33. package/dist/engine.d.ts +49 -0
  34. package/dist/engine.js +208 -0
  35. package/dist/external.d.ts +10 -0
  36. package/dist/external.js +56 -0
  37. package/dist/home.d.ts +31 -0
  38. package/dist/home.js +108 -0
  39. package/dist/ids.d.ts +12 -0
  40. package/dist/ids.js +44 -0
  41. package/dist/keys.d.ts +124 -0
  42. package/dist/keys.js +309 -0
  43. package/dist/lib.d.ts +9 -0
  44. package/dist/lib.js +31 -0
  45. package/dist/liveness.d.ts +23 -0
  46. package/dist/liveness.js +221 -0
  47. package/dist/main.d.ts +3 -0
  48. package/dist/main.js +155 -0
  49. package/dist/narrate.d.ts +19 -0
  50. package/dist/narrate.js +124 -0
  51. package/dist/podman.d.ts +46 -0
  52. package/dist/podman.js +254 -0
  53. package/dist/projects.d.ts +70 -0
  54. package/dist/projects.js +232 -0
  55. package/dist/resolve.d.ts +27 -0
  56. package/dist/resolve.js +138 -0
  57. package/dist/sandbox.d.ts +36 -0
  58. package/dist/sandbox.js +104 -0
  59. package/dist/scaffold.d.ts +29 -0
  60. package/dist/scaffold.js +220 -0
  61. package/dist/session.d.ts +77 -0
  62. package/dist/session.js +156 -0
  63. package/dist/term.d.ts +69 -0
  64. package/dist/term.js +242 -0
  65. package/dist/tui/app.d.ts +8 -0
  66. package/dist/tui/app.js +257 -0
  67. package/dist/tui/theme.d.ts +23 -0
  68. package/dist/tui/theme.js +134 -0
  69. package/dist/tui/wrap.d.ts +12 -0
  70. package/dist/tui/wrap.js +62 -0
  71. package/dist/validate.d.ts +145 -0
  72. package/dist/validate.js +959 -0
  73. package/package.json +76 -0
  74. package/templates/.github/copilot-instructions.md +1579 -0
  75. package/templates/.github/prompts/new-agent.prompt.md +38 -0
  76. package/templates/.github/prompts/new-skill.prompt.md +37 -0
  77. package/templates/.github/prompts/review-project.prompt.md +31 -0
  78. package/templates/.github/skills/zen-cli/SKILL.md +110 -0
@@ -0,0 +1,1579 @@
1
+ # Copilot instructions — agent projects
2
+
3
+ > Written by `zen init` into `.github/copilot-instructions.md`, alongside the
4
+ > prompt files and skills in the same tree (§2.4). VS Code loads it
5
+ > automatically for every request in this folder, so it is the standing brief
6
+ > for anyone — human or agent — editing this project.
7
+ >
8
+ > It describes the Zenera Neo runtime, not this particular project, and `zen`
9
+ > rewrites the whole tree on `init` and `open` — so put this project's own
10
+ > conventions in `INSTRUCTIONS.md`, where they will survive. The runtime's own
11
+ > reference is `docs/agents-yaml.md`, `docs/projects.md` and `DESIGN.md` in the
12
+ > @zenera/neo repository — where they disagree with this file, they win.
13
+
14
+ ---
15
+
16
+ ## 0. What this repository is
17
+
18
+ This is **not a normal application repository**. There is no application code
19
+ here. It is a folder of declarative artefacts — YAML and Markdown — that
20
+ assemble into a running multi-agent system, driven by the `zen` CLI.
21
+
22
+ The centre of gravity is **prose**. Every valuable change here is an edit to a
23
+ prompt, a skill, or one line of `agents.yaml`. Behaviour is configured, not
24
+ programmed: what an agent knows, which model answers, and which of the tools
25
+ `zen run` provides it may reach for.
26
+
27
+ **Default posture when working in this repo:**
28
+
29
+ 1. Find which artefact owns the behaviour before editing anything (§10).
30
+ 2. Prefer editing a prompt or skill over adding an agent.
31
+ 3. Prefer adding a skill over lengthening a prompt.
32
+ 4. Prefer granting a tool over asking the model to compute or remember.
33
+ 5. Never add an agent to solve a problem that is really a prompt problem.
34
+ 6. Every change must still load: `agents.yaml` is validated strictly at load,
35
+ and `zen check` says so before a model is ever called.
36
+
37
+ ---
38
+
39
+ ## 1. Mental model
40
+
41
+ ### 1.1 What an agent actually is
42
+
43
+ An agent is four things and nothing more:
44
+
45
+ | Part | Where it lives | What it decides |
46
+ | --------------- | ---------------------------------------------- | ----------------------------- |
47
+ | **Instruction** | `INSTRUCTIONS.md` + `agents/prompts/<name>.md` | How it behaves |
48
+ | **Model** | `agents.yaml` → `model:` | How well and how expensively |
49
+ | **Tools** | `agents.yaml` → `tools:` | What it can _do_ |
50
+ | **Knowledge** | `agents/skills/*` + memory | What it can _know_, on demand |
51
+
52
+ Plus two relations: **handoffs** — which other agents it may transfer control to
53
+ — and **fork** — whether it may split into parallel branches at all (§6.4).
54
+
55
+ There is no hidden orchestration layer. If behaviour is wrong, one of those six
56
+ things is wrong.
57
+
58
+ ### 1.2 The loop
59
+
60
+ ```
61
+ user input
62
+
63
+ [system prompt][tool schemas][transcript] → model
64
+
65
+ model returns: text → done
66
+ tools → execute → append results → loop
67
+ handoff→ switch agent, re-render system prompt → loop
68
+ fork → run N branches in parallel → join → loop
69
+ ```
70
+
71
+ Every step appends to an **append-only trajectory**. Nothing is ever mutated or
72
+ deleted; compaction _covers_ older nodes rather than removing them, so the audit
73
+ trail survives context pressure. A conversation _is_ its `AgentState`; the loaded
74
+ project is immutable and shared by every conversation in the process.
75
+
76
+ ### 1.3 The single most important idea: context is the product
77
+
78
+ The model sees exactly one thing: a token sequence. Your entire job as an author
79
+ is deciding **what is in that sequence and in what order**.
80
+
81
+ ```
82
+ ┌─────────────────────────────────────────────┐
83
+ │ tool schemas fixed at load │ ← stable, cacheable
84
+ │ INSTRUCTIONS.md shared by all agents │ ← stable, cacheable
85
+ │ agent prompt this agent's brief │ ← stable per agent
86
+ │ skill index names + descriptions │ ← stable per agent
87
+ │ preloaded skills activated turn 0 │ ← stable, in the cached prefix
88
+ ├─────────────────────────────────────────────┤
89
+ │ transcript grows │ ← the volatile part
90
+ └─────────────────────────────────────────────┘
91
+ ```
92
+
93
+ Two consequences that drive nearly every design rule in this document:
94
+
95
+ - **Stable prefix = cache hit.** Providers cache by prefix. Anything appended
96
+ mid-run _after_ the first reply — a tool schema, an instruction, a late skill
97
+ activation — invalidates the cache from that point on. This is why tool schemas
98
+ are fixed at load and skill-owned tools are declared from turn 0 and merely
99
+ _gated_ at call time.
100
+ - **Everything in the prefix is paid for on every call.** A 4 000-token prompt
101
+ covering twelve perils costs on every turn, whether or not the case involves
102
+ any of them. Progressive disclosure (§5) is not an optimisation; it is the
103
+ organising principle.
104
+
105
+ ### 1.4 What a good system looks like
106
+
107
+ - Each agent has **one job you can state in one sentence**.
108
+ - The prompt says what to do, not what the software is.
109
+ - Facts live in skills or tools, never in a prompt that must be edited to change
110
+ a number.
111
+ - Numbers come from tools; the model narrates, it does not compute.
112
+ - The failure mode of every instruction is stated ("if X is absent, say so and
113
+ stop") — an unstated failure mode is an invented one.
114
+
115
+ ---
116
+
117
+ ## 2. Repository layout
118
+
119
+ ### 2.1 Canonical
120
+
121
+ ```
122
+ my-project/
123
+ ├── .github/ the editor's brief — §2.4
124
+ │ ├── copilot-instructions.md this file
125
+ │ ├── prompts/*.prompt.md tasks you invoke by name
126
+ │ └── skills/*/SKILL.md reference the editor loads on demand
127
+ ├── .env credentials — NEVER committed
128
+ ├── INSTRUCTIONS.md house rules, prepended to every agent
129
+ ├── agents.yaml who exists, what they may reach for
130
+ ├── agents/
131
+ │ ├── prompts/
132
+ │ │ ├── intake.md one agent's own brief
133
+ │ │ └── adjuster.md
134
+ │ └── skills/
135
+ │ ├── house_style/
136
+ │ │ ├── SKILL.md folder skill
137
+ │ │ └── examples.md sibling files become `resources`
138
+ │ ├── water_damage/
139
+ │ │ └── SKILL.md
140
+ │ └── shipping_delays.md flat skill (frontmatter + body)
141
+ └── sessions/ run state, memory, whatever the agent wrote
142
+ ```
143
+
144
+ Only `agents.yaml` is required, and only `agents:` is required inside it.
145
+ The config is found by name, in order: `agents.yaml`, `agents.yml`,
146
+ `agents/agents.yaml`, `agents/agents.yml`.
147
+
148
+ ### 2.2 Variants
149
+
150
+ **Single agent, knowledge-heavy** — the most under-used shape. One agent, one
151
+ prompt, a large skill catalog. Prefer this until routing is genuinely needed.
152
+
153
+ ```
154
+ INSTRUCTIONS.md · agents.yaml · agents/prompts/assistant.md · agents/skills/** (20 skills)
155
+ ```
156
+
157
+ **Router + specialists** — a cheap intake agent that classifies and hands off.
158
+
159
+ ```
160
+ agents/prompts/{intake,billing,technical,escalation}.md
161
+ ```
162
+
163
+ **Pipeline** — fixed stages, each handing to the next; the last one answers.
164
+
165
+ ```
166
+ research → draft → review
167
+ ```
168
+
169
+ **Shared skills across projects** — `skills:` accepts a list, merged into one
170
+ catalog:
171
+
172
+ ```yaml
173
+ skills:
174
+ - agents/skills
175
+ - ../shared/compliance-skills
176
+ ```
177
+
178
+ Paths may not escape the project root unless the root is set to the common
179
+ ancestor. Several directories become **one** provider (id `project`) — "which
180
+ folder is this skill in?" is not a question an author should have to answer.
181
+
182
+ ### 2.3 Naming rules (enforced)
183
+
184
+ Agent names, provider names and model alias keys must match:
185
+
186
+ ```
187
+ ^[a-z0-9]+(?:[-_][a-z0-9]+)*$
188
+ ```
189
+
190
+ They reach the model as `transfer_to_<name>` and the file system as directory
191
+ names, so: `intake`, `order-triage`, `house_style`. No spaces, no capitals, no
192
+ dots.
193
+
194
+ ### 2.4 The two audiences
195
+
196
+ There are two sets of instructions in this repository and they are not for the
197
+ same reader. Keeping them apart is the single easiest thing to get wrong.
198
+
199
+ | Tree | Read by | About |
200
+ | ---------------------------- | ------------------------------------------ | ----------------------------------- |
201
+ | `INSTRUCTIONS.md`, `agents/` | the **project's** agents, at run time | the domain this system works in |
202
+ | `.github/` | the **editor's** assistant, while you edit | how a project of this kind is built |
203
+
204
+ The `.github/` tree follows the same progressive-disclosure discipline the
205
+ agents do, for the same reason — it is a prefix somebody pays for:
206
+
207
+ - **`copilot-instructions.md`** is always on. Everything in it is loaded for
208
+ every request in this folder, so it holds only what is true of every task.
209
+ - **`.github/skills/<name>/SKILL.md`** is reference the editor loads when its
210
+ `description` matches what you asked. Put long, occasional material here —
211
+ a command surface, a vendor's quirks, a format spec — not in the file above.
212
+ The `description` is the routing key; §3.4 applies to these as much as to the
213
+ project's own skills.
214
+ - **`.github/prompts/<name>.prompt.md`** is a task you invoke by name (`/name`),
215
+ with `mode: agent` and a `description` in its frontmatter. Write one when a
216
+ job is done repeatedly and has a right order — adding an agent, adding a
217
+ skill, reviewing before a commit.
218
+
219
+ `zen init` and `zen open` rewrite this whole tree from the version of `zen` in
220
+ hand, so **edits inside `.github/` do not survive**. Project-specific conventions
221
+ belong in `INSTRUCTIONS.md` and the agent prompts, which are never overwritten.
222
+
223
+ ---
224
+
225
+ ## 3. File formats
226
+
227
+ ### 3.1 `agents.yaml`
228
+
229
+ The schema is **strict**: an unknown key is a load error, not a value silently
230
+ ignored. Errors name the path — `agents.yaml: agents[1].skills.discovery — …`.
231
+
232
+ ```yaml
233
+ version: 1 # schema version, defaults to 1
234
+ default: intake # entry agent; wins over any `default: true`
235
+
236
+ providers: {} # named connections (credentials + endpoint)
237
+ provider: openai # the provider an unprefixed model id belongs to — §7.3
238
+ models: {} # named model configurations
239
+ model: fast # fallback for agents that do not pin their own
240
+ embeddings: {} # named vectorisers — §3.1.1
241
+ embedding: small # the one `AgentProject.embedder()` returns when asked for no name
242
+ skills: agents/skills # one directory, or a list
243
+
244
+ agents: # the only required key; at least one entry
245
+ - name: intake
246
+ description: Takes the first message, gets the reference, routes the case.
247
+ system: agents/prompts/intake.md
248
+ model: router
249
+ tools: [policy_lookup]
250
+ handoffs: [adjuster]
251
+ ```
252
+
253
+ **Agent fields**
254
+
255
+ | Field | Meaning |
256
+ | ------------- | ------------------------------------------------------------------------------------ |
257
+ | `name` | **Required.** See §2.3 |
258
+ | `description` | What a sibling's `transfer_to_<name>` tool tells the model. Write it _for the model_ |
259
+ | `system` | Prompt path, relative to root. Defaults to `agents/prompts/<name>.md` if present |
260
+ | `model` | A `models:` alias or shorthand. Falls back to top-level `model:` |
261
+ | `tools` | Selectors over the tools `zen run` provides — see §3.6 |
262
+ | `handoffs` | Agent names this one may transfer to. Bare strings; no per-edge config |
263
+ | `skills` | Skill binding — see §5.2 |
264
+ | `fork` | `true`, or `{ agents, maxBranches }` — opt-in to parallel branches; see §6.4 |
265
+ | `default` | `true` marks the entry point when no top-level `default:` |
266
+
267
+ **Providers** — a provider is a _connection_, not a model. One client is built
268
+ per name and shared, so five agents on one key open one connection pool.
269
+
270
+ | Field | Meaning |
271
+ | ------------------------- | --------------------------------------------------------------------------------------------------------- |
272
+ | `kind` | `openai` \| `google` \| `vertex` \| `anthropic` \| `openrouter` \| `openai-compatible` (default `openai`) |
273
+ | `apiKey` / `apiKeyEnv` | Literal, `${VAR}`, or the name of the env var holding it |
274
+ | `baseURL` / `baseURLEnv` | For gateways and compatible endpoints |
275
+ | `project` / `location` | **vertex only** — GCP project id and region (or `global`) |
276
+ | `headers` | Sent on every request: routing, attribution, api versions |
277
+ | `timeoutMs`, `maxRetries` | Per-request timeout and retry count |
278
+
279
+ `openai`, `google`, `vertex`, `anthropic`, `openrouter` and `openai-compatible`
280
+ are usable as provider _names_ with no declaration at all. Declare a
281
+ `providers:` entry only when it says something the default does not — a second
282
+ key, a region, a base url.
283
+
284
+ `${VAR}` and `${VAR:-fallback}` expand from the environment, compose inside
285
+ longer values (`https://${GATEWAY}/v1`), and are **lazy**: a declared-but-unused
286
+ provider with a missing key does not fail loading.
287
+
288
+ **Errors caught at load** — rely on these instead of defensive checks:
289
+
290
+ - any unknown key; any name breaking the pattern
291
+ - `models.<alias>.provider` naming an undeclared provider
292
+ - `embeddings.<alias>.provider` naming an undeclared provider
293
+ - `agents[].tools` naming a tool the runtime does not provide, or a group with nothing in it
294
+ - `agents[].handoffs` naming an unknown agent, or the agent itself
295
+ - `agents[].skills.provider` / `.allow` / `.preload` naming something absent
296
+ - a `preload:` entry missing from `allow:`
297
+ - `agents[].fork.agents` naming an unknown agent, or being empty; `maxBranches` below 2
298
+ - `system:` pointing at a missing file, or outside the project root
299
+
300
+ **Not caught at load** — a model id whose prefix is missing and so resolves to the
301
+ wrong provider (§7.3), and any combination of knobs the vendor rejects at request
302
+ time, such as OpenAI reasoning on chat completions (§7.6). Both surface on the
303
+ first call, so read §7 before writing a `models:` entry.
304
+
305
+ **Comments in `agents.yaml`** — this file is the architecture diagram of the
306
+ project, and its comments are read by whoever has to change it next. Write them
307
+ at that level:
308
+
309
+ - Say **what a block is for** and **why it exists**: what this agent owns, why
310
+ this one is on the deep tier, why this handoff edge is there.
311
+ - Keep them **short** — one line above a block, a few words at the end of a line.
312
+ A comment longer than the thing it describes is a design doc in the wrong file.
313
+ - Do **not** restate the runtime. How skills are discovered, how the trajectory
314
+ is appended, how prompt caching works, what the loader validates — none of that
315
+ belongs here. It is documented in this file and in `docs/agents-yaml.md`.
316
+ - Do **not** restate the key. `# the model this agent uses` above `model:` is
317
+ noise; `# cheap: it only classifies` is not.
318
+ - Do not narrate edits (`# added 2026-08`, `# was gpt-4o`). Git owns that.
319
+
320
+ ```yaml
321
+ # Intake classifies and routes; it never answers.
322
+ - name: intake
323
+ model: router # cheap tier — one sentence in, one handoff out
324
+ handoffs: [adjuster, escalation]
325
+ ```
326
+
327
+ ### 3.1.1 `embeddings:`
328
+
329
+ A vectoriser turns text into a vector, for retrieval rather than for answering.
330
+ It resolves through the **same `providers:`**, so a key declared once generates
331
+ and embeds without being written twice.
332
+
333
+ ```yaml
334
+ providers:
335
+ house:
336
+ apiKey: ${ACME_OPENAI_KEY}
337
+
338
+ embeddings:
339
+ small: openai:text-embedding-3-small # a shorthand string...
340
+ large: # ...or the object form
341
+ provider: house
342
+ model: text-embedding-3-large
343
+ dimensions: 256
344
+
345
+ embedding: small
346
+ ```
347
+
348
+ | Field | Meaning |
349
+ | ------------------------ | ------------------------------------------------------------------------ |
350
+ | `provider` | A `providers:` name or a built-in kind. Defaults to the default provider |
351
+ | `model` | **Required.** The bare id — the object form never re-parses a shorthand |
352
+ | `dimensions` | Truncate to this width, where the model supports it |
353
+ | `apiKey` / `baseURL` / … | The same credential fields a provider takes, for a one-off connection |
354
+ | `title` | **gemini only** — a document title the retrieval task type weighs |
355
+ | `maxBatch` | **gemini only** — texts per request; see below |
356
+ | `routing` | **openrouter only** — which upstream provider serves the request |
357
+
358
+ Four things differ from `models:` and are worth knowing before you write one:
359
+
360
+ - **No `api:` field.** `/v1/responses` has no embeddings endpoint, so naming an
361
+ api means nothing on any protocol. The shorthand is `[provider:]model`.
362
+ - **`kind: anthropic` has no embeddings API at all.** Anthropic publishes none
363
+ and points at third parties; an `embeddings:` entry on an Anthropic provider
364
+ fails at load. Use another provider — the connection need not be the one the
365
+ agents talk through.
366
+ - **Not a per-agent key.** Nothing in the runtime consumes a vectoriser yet, so
367
+ there is no `agents[].embedding:`. A TypeScript host reaches one with
368
+ `project.embedder()` for the default, or `project.embedder('large')` by name.
369
+ - **Vectors come back unit length**, so cosine and dot product agree. This is a
370
+ guarantee of the runtime, not of the vendor: truncating with `dimensions:` is a
371
+ raw slice and only some models rescale afterwards — `gemini-embedding-2` does,
372
+ `gemini-embedding-001` returns |v| ≈ 0.58 at 768 of its 3072. Pass
373
+ `normalize: false` on a call to see what the model actually said.
374
+
375
+ Google's `embedContent` takes one document per request for every
376
+ `gemini-embedding-*` model, and the adapter splits a batch across requests to
377
+ hide that. `maxBatch` therefore defaults to `1`; raise it only for a
378
+ `text-embedding-*` model, which accepts more.
379
+
380
+ `zen check` and `zen models` report every declared embedding beside the models,
381
+ with the credential each one would need.
382
+
383
+ ### 3.2 `INSTRUCTIONS.md`
384
+
385
+ House rules, read **once** and prepended to every agent's system prompt. It is
386
+ the stable head of the cached prefix, so it should change rarely. The name is
387
+ deliberately not `AGENTS.md` — that one belongs to the coding assistant reading
388
+ this file, and these rules address the project's own agents.
389
+
390
+ Put here only what is true for **every** agent:
391
+
392
+ - identity and domain ("You work the property claims desk")
393
+ - non-negotiable prohibitions (regulatory, legal, safety)
394
+ - global format and tone constraints
395
+ - domain vocabulary and identifier formats
396
+
397
+ Do **not** put here: anything one agent needs and another does not; anything that
398
+ changes weekly; long reference data (that is a skill).
399
+
400
+ **When writing it, remember what it is: a prefix on every agent's system prompt.**
401
+ It is not a README and not a design document — every line is paid for on every
402
+ call, by every agent, and each one is an instruction the model will try to
403
+ follow. So:
404
+
405
+ - It is a **prompt**: §4.2 applies in full — second person, imperative, no
406
+ hedging, no meta-talk about the runtime, failure paths stated.
407
+ - Carry the **shared model of the project**: what this system is, what the agents
408
+ are collectively for, how the work flows between them, and the vocabulary and
409
+ identifier formats they all use. Enough for any agent to know where it sits;
410
+ not a tour of the codebase.
411
+ - Describe the architecture in **one short paragraph or a handful of lines**, in
412
+ terms the agents can act on ("the adjuster owns coverage decisions; you do
413
+ not"), not in terms of files, YAML keys or the runtime.
414
+ - Anything only one agent needs goes in that agent's prompt instead. If you find
415
+ yourself writing "if you are the router…", you are in the wrong file.
416
+
417
+ Target 20–60 lines. If it exceeds ~100, split the stable half out into a
418
+ preloaded skill.
419
+
420
+ ### 3.3 `agents/prompts/<name>.md`
421
+
422
+ Plain Markdown, no frontmatter. This is the agent's _job description_, appended
423
+ after `INSTRUCTIONS.md`.
424
+
425
+ Structure that works:
426
+
427
+ ```markdown
428
+ <one sentence: who this agent is and what it owns>
429
+
430
+ <what it does, as a short numbered procedure or 3–5 rules>
431
+
432
+ <what it must NOT do — especially the neighbouring agent's job>
433
+
434
+ <how to finish: the shape of the answer, or which handoff ends the turn>
435
+ ```
436
+
437
+ Real example (an intake agent, complete):
438
+
439
+ ```markdown
440
+ You are the first person a claimant reaches.
441
+
442
+ Your job is small and you should finish it fast:
443
+
444
+ 1. Read what happened.
445
+ 2. Call `policy_lookup` with the claim reference to confirm the policy exists
446
+ and see what it covers.
447
+ 3. Hand the case to the `adjuster` agent, which owns the peril policies.
448
+
449
+ Do not quote coverage rules yourself — you do not have them. Do not ask the
450
+ claimant for anything the message already contains.
451
+ ```
452
+
453
+ Note what makes it work: it is 9 lines; it names the tool and the handoff
454
+ literally; it states the boundary ("you do not have them") with the _reason_; it
455
+ forbids the specific failure that agent actually exhibits.
456
+
457
+ Write it as a prompt, under the rules in §4.2 — **instructive and concise**. It
458
+ tells one agent what to do; it never explains the system (`INSTRUCTIONS.md`
459
+ already did, §3.2), never repeats a house rule, and never describes the runtime.
460
+ Every line should be an instruction the model can act on or a boundary it can
461
+ check itself against.
462
+
463
+ Target 10–40 lines. A 200-line prompt is a skill catalog that has not been split
464
+ yet.
465
+
466
+ ### 3.4 Skills
467
+
468
+ A skill is curated, reusable instruction content — plus optional tools — loaded
469
+ **on demand** instead of permanently occupying the system prompt. Two layouts,
470
+ discovered in the same scan:
471
+
472
+ ```
473
+ agents/skills/refund_policy.md flat: frontmatter + body
474
+ agents/skills/refund_policy/SKILL.md folder: sibling files become `resources`
475
+ ```
476
+
477
+ Frontmatter is a deliberately small subset of YAML — `key: value`, plus `[a, b]`
478
+ flow lists for `tags` and `tools`. **Every key is optional**: `name` defaults to
479
+ the file/folder name, `description` to the first non-empty line of the body.
480
+
481
+ ```markdown
482
+ ---
483
+ name: refund_policy
484
+ description: When a refund is owed, and how much.
485
+ version: 2.0.0
486
+ tags: [billing, policy]
487
+ tools: [issue_refund]
488
+ ---
489
+
490
+ A parcel counts as late once it passes its promised delivery date by 48 hours.
491
+
492
+ - **48h to 7 days late** — apologise, confirm the parcel is still moving, and
493
+ refund the shipping fee. Do not refund the goods.
494
+ - **More than 7 days late** — treat the parcel as lost. Offer a replacement at
495
+ no cost, or a full refund, whichever the customer prefers.
496
+
497
+ Never give a new delivery date. Say "still in transit" instead.
498
+ ```
499
+
500
+ **The `description` is the routing key.** It is the only thing the model sees
501
+ before deciding to load the skill. Write it as the _condition under which this
502
+ skill is needed_, not as a title:
503
+
504
+ | Bad | Good |
505
+ | --------------------------- | ------------------------------------------------------------------------------------ |
506
+ | `description: Refunds` | `description: When a refund is owed, and how much.` |
507
+ | `description: Water policy` | `description: Escape of water from plumbing and tanks — and the freezing exclusion.` |
508
+
509
+ **`tools:` in frontmatter** names skill-owned tools. They are declared to the
510
+ provider from turn 0 (the schema never changes) but **refuse to execute** until
511
+ the skill is active. This is how a tool can be gated without breaking the cache.
512
+
513
+ Use a folder skill when the content needs companions — a CSV rate table, an
514
+ example letter, a JSON schema. Siblings become `resources` the model can read.
515
+
516
+ ### 3.5 Tools
517
+
518
+ A tool is what an agent can _do_ rather than say. `zen run` provides three
519
+ groups — the workspace tools (§3.6), the sandbox tools (§3.7) and the web tools
520
+ (§3.8) — and `agents.yaml` decides which agent holds which. Nothing else reaches
521
+ the machine, so `tools:` is the whole permission model: an agent that does not
522
+ name a tool cannot use it, whatever its prompt says.
523
+
524
+ Two rules follow:
525
+
526
+ - **Grant the narrowest set the job needs.** An agent that only reviews should
527
+ not be holding the tools that overwrite files.
528
+ - **Say in the prompt when to reach for what.** A granted tool the prompt never
529
+ mentions is used at the model's discretion, which is not the same as never.
530
+
531
+ Skills can own tools too — `tools:` in a skill's frontmatter (§3.4) names tools
532
+ that refuse to run until that skill is active.
533
+
534
+ ### 3.6 The workspace tools (`workspace:*`)
535
+
536
+ `zen run` builds this set for you, rooted at the session's workspace directory.
537
+ Nothing else reaches the file system, so an agent whose `tools:` does not name
538
+ them cannot see a file at all.
539
+
540
+ | Tool | What it does |
541
+ | ------------- | ---------------------------------------------------------------------------------------- |
542
+ | `read_file` | Reads text; `start_line`/`end_line` for a range. Reports total lines and if it truncated |
543
+ | `list_dir` | Entries with kind, format (text/binary/image/…), size, and line count for text |
544
+ | `find_files` | Paths containing a substring, case-insensitive |
545
+ | `write_file` | Creates or overwrites a whole file, making parent directories |
546
+ | `apply_patch` | Edits by surrounding context rather than line numbers; several files atomically |
547
+ | `move_file` | Moves or renames; refuses to clobber without `overwrite` |
548
+ | `delete_file` | Deletes; a directory needs `recursive` |
549
+
550
+ Every path is resolved through one containment gate — symlinks followed, then
551
+ checked — so nothing outside the workspace root is reachable. Reads are capped
552
+ and listings bounded, so no single call can flood the context.
553
+
554
+ Paths are written relative to the root, and `/workspace/...` — the name the same
555
+ directory has inside the sandbox (§3.7) — is accepted as well, so a path copied
556
+ out of a command's output does not have to be translated first. When a mount is
557
+ configured the tools also _report_ that name, so the two toolsets speak one
558
+ vocabulary and a path can be passed from either to the other unchanged; with no
559
+ container involved there is no second name and everything stays relative.
560
+
561
+ **Selecting them.** A `tools:` entry is a selector, not only a name:
562
+
563
+ | Selector | Selects |
564
+ | ------------- | ------------------------------------------------- |
565
+ | `read_file` | that one tool |
566
+ | `workspace:*` | every tool in the group |
567
+ | `'*'` | every tool the runtime provides |
568
+ | `-<any>` | removes what it matches from the selection so far |
569
+
570
+ ```yaml
571
+ agents:
572
+ - name: editor
573
+ tools: [workspace:*]
574
+
575
+ - name: reviewer
576
+ # Everything except the four that can change something.
577
+ tools: [workspace:*, -write_file, -apply_patch, -move_file, -delete_file]
578
+ ```
579
+
580
+ Selectors apply in the order written, so a `-` line reads as an exception to the
581
+ line above it. Quote a lone `'*'`: unquoted, YAML reads it as an alias and
582
+ refuses the file. `workspace:*` needs no quoting. There is no name globbing —
583
+ `read_*` is an unknown tool, because a selector should track a declared set, not
584
+ a naming habit.
585
+
586
+ `zen run --read-only` withholds the four mutating tools whatever `agents.yaml`
587
+ asks for: the deployment overriding the repository, as everywhere else.
588
+
589
+ **Prompting for them.** Three lines earn their place in any prompt that grants
590
+ this group:
591
+
592
+ - Read before editing — `apply_patch` matches on exact text, so a patch built
593
+ from memory fails.
594
+ - Prefer `apply_patch` to `write_file` for an existing file. Rewriting a file to
595
+ change one line costs the whole file in output tokens and loses everything the
596
+ model did not think to repeat.
597
+ - `list_dir` before guessing a path; `find_files` when the name is known but the
598
+ location is not.
599
+
600
+ ### 3.7 The sandbox tools (`sandbox:*`)
601
+
602
+ The second group `zen run` builds. These run a shell command in a Linux
603
+ container with the same workspace mounted at `/workspace`, so an agent can
604
+ build, test, install and inspect rather than only read and write.
605
+
606
+ | Tool | What it does |
607
+ | ------------------------ | ------------------------------------------------------------------------ |
608
+ | `run_command` | Runs to completion. Returns exit code, stdout, stderr, duration |
609
+ | `run_command_background` | Starts a long process (a server, a watch build) and returns a `job_id` |
610
+ | `read_command_output` | A window of a job's output from `start_line`, plus whether it still runs |
611
+ | `stop_command` | Signals a job's process group |
612
+
613
+ The container is the boundary. Nothing inspects the command — there is no
614
+ allow-list of binaries and no pattern matching on what the model wrote, because
615
+ both are trivially defeated and neither survives a shell. What holds is that
616
+ only the workspace and the session's `/home/agent` are mounted, the container
617
+ is removed at the end of the session, and the command is never a shell argument
618
+ on the host: it travels on stdin to `/bin/sh` inside.
619
+
620
+ **Configuring it.** A top-level `sandbox:` block in `agents.yaml` describes the
621
+ container. Every field has a default, so the block is optional — write only the
622
+ lines that differ:
623
+
624
+ ```yaml
625
+ sandbox:
626
+ persist: true # recommended — see below
627
+ image: docker.io/library/python:3.14-slim-bookworm # the default
628
+ cpus: 4 # fractional cores
629
+ memory: 4096 # MiB
630
+ network: bridge # `none` for a project that must not reach out
631
+ timeout: 300 # seconds per command
632
+ env: [HTTPS_PROXY, NO_PROXY] # host variables to forward, by NAME
633
+ ```
634
+
635
+ | Field | Default | Meaning |
636
+ | --------- | --------------------------------------------- | -------------------------------------------- |
637
+ | `image` | `docker.io/library/python:3.14-slim-bookworm` | The base image commands run in |
638
+ | `cpus` | the host's | Fractional cores |
639
+ | `memory` | the host's | MiB |
640
+ | `network` | `bridge` | `bridge` / `none` / `host` |
641
+ | `workdir` | `/workspace` | Mount point and default cwd |
642
+ | `timeout` | `120` | Seconds per command |
643
+ | `user` | the image's | uid, name, or `uid:gid` |
644
+ | `persist` | `false` — **set it to `true`** | Keep the container between runs of a session |
645
+ | `env` | none | Host variables to forward, **names** |
646
+
647
+ `env:` takes **names, never values** — a value here would be a secret in the
648
+ repository — and anything credential-shaped (`KEY`, `TOKEN`, `SECRET`,
649
+ `PASSWORD`, `CREDENTIAL`) is refused at load.
650
+
651
+ Agents share one container, because they share the workspace and a hand-off is
652
+ meant to be continuous. An agent that needs something else says so and gets its
653
+ own, with its block merged over the top-level one:
654
+
655
+ ```yaml
656
+ sandbox:
657
+ image: docker.io/library/python:3.14-slim-bookworm
658
+
659
+ agents:
660
+ - name: builder
661
+ tools: [workspace:*, sandbox:*]
662
+ sandbox:
663
+ image: docker.io/library/node:22-bookworm-slim
664
+ memory: 8192
665
+ - name: analyst
666
+ tools: [workspace:*, sandbox:*] # shares the project's container
667
+ ```
668
+
669
+ **Write `persist: true` unless you have a reason not to.** By default the
670
+ container is _removed_ when the session closes, and only two paths survive it:
671
+ `/workspace`, and `/home/agent` — which is `$HOME` inside, backed by the session
672
+ directory. That covers `pip install --user`, `npm config` and `~/.cache`, but it
673
+ does **not** cover the ordinary thing an agent actually does: `pip install X` or
674
+ `apt-get install X` as root writes to the container's system paths, and those
675
+ are gone on the next `zen run`. The agent then reinstalls, silently, every
676
+ single time — and usually does not realise it has, because the previous run's
677
+ transcript says it succeeded.
678
+
679
+ ```yaml
680
+ sandbox:
681
+ persist: true
682
+ ```
683
+
684
+ With it, the container is _stopped_ rather than removed, and the next run of
685
+ that session starts the same one back up with everything still installed. The
686
+ cost is containers that outlive their sessions — `zen sandbox status` lists them
687
+ and `zen sandbox clean` removes them.
688
+
689
+ Changing any field renames the container, so bumping the image gets a fresh one
690
+ rather than an old one quietly persisting with the wrong contents. That is also
691
+ the one sharp edge of `persist: true`: a config change abandons the old
692
+ container with whatever was installed in it, so a long-lived setup still belongs
693
+ in `image:` rather than in an accumulated rootfs.
694
+
695
+ Granting the group is what makes the project need Podman: `zen run` checks the
696
+ engine before the first turn and exits `5` with an install command if it is
697
+ missing. `zen sandbox status` answers the same question on its own. Full
698
+ reference: `docs/agents-yaml.md`.
699
+
700
+ **Prompting for them.** Two lines earn their place:
701
+
702
+ - The workspace is at `/workspace` and is the same directory the file tools
703
+ see — an edit made with `apply_patch` is what a command will compile, and a
704
+ path from either side works on both.
705
+ - Anything that does not return, returns — use `run_command_background` for a
706
+ server, not `run_command` with a large timeout.
707
+
708
+ ### 3.8 The web tools (`exa:*`)
709
+
710
+ The third group `zen run` builds. These reach the live web through
711
+ [Exa](https://exa.ai) — a search index built for models rather than for people,
712
+ so a query is a sentence describing what is wanted, not a bag of keywords.
713
+
714
+ | Tool | What it does |
715
+ | ------------ | ------------------------------------------------------------------------------- |
716
+ | `web_search` | Ranked pages for a described query, each with a short excerpt of why it matched |
717
+ | `web_read` | The readable text of pages, several at once, boilerplate stripped |
718
+ | `web_answer` | A written answer to a question, with the sources it was drawn from |
719
+
720
+ The three are meant to be used in that order: **search to find, read to quote.**
721
+ An excerpt is enough to judge which source to trust and never enough to cite
722
+ from — `web_search` returns the sentences that made a page match, not the page.
723
+ `web_answer` runs a search _and_ a model on the other side, so it is the slowest
724
+ and dearest of the three; it earns its cost when the answer is a fact spread
725
+ over several pages, and wastes it when a specific document is wanted.
726
+
727
+ ```yaml
728
+ agents:
729
+ - name: researcher
730
+ tools: [exa:*, workspace:*]
731
+
732
+ - name: fact-checker
733
+ # Find and read, but never let a model on the far side do the reasoning.
734
+ tools: [web_search, web_read]
735
+ ```
736
+
737
+ **The key.** All three read `$EXA_API_KEY` **when they are called**, not when the
738
+ project loads. So a project naming `exa:*` still loads on a machine that has no
739
+ key — the tools simply refuse, on the turn that tried, saying which variable is
740
+ missing. Get a key from <https://dashboard.exa.ai/api-keys> and hold it in
741
+ either place:
742
+
743
+ ```
744
+ zen key add exa # the keyring; materialised into the environment per run
745
+ EXA_API_KEY=... # or .env, which wins over the keyring
746
+ ```
747
+
748
+ `zen check` warns when an agent selects one of these tools and neither place
749
+ holds a key. Unlike a model credential this is a warning, not an error: the
750
+ project is still valid, it just cannot search yet.
751
+
752
+ **Notable arguments.** Defaults are chosen so that the common call is
753
+ `{ "query": "…" }` and nothing else:
754
+
755
+ | Argument | On | Why it exists |
756
+ | ------------------------------------------ | ------------ | ------------------------------------------------------------------------------- |
757
+ | `num_results` | `web_search` | 8 by default, 25 at most |
758
+ | `include_domains` / `exclude_domains` | `web_search` | The replacement for `site:` — operators in the query text do not work here |
759
+ | `start_published_date` / `end_published_…` | `web_search` | ISO 8601. The only reliable way to exclude a stale answer |
760
+ | `category` | `web_search` | `company`, `publication`, `news`, `personal site`, `financial report`, `people` |
761
+ | `max_characters` | `web_read` | 4 000 by default, 10 000 at most; `truncated` says when a page was cut |
762
+ | `max_age_hours` | `web_read` | `0` forces a live crawl. Omit it unless the page changes by the hour |
763
+
764
+ Every reply is bounded — pages are cut at the cap and the whole call at 128 KiB
765
+ of text — so one call cannot flood the context. Each carries `cost_usd`, which
766
+ is what the vendor charged for that call.
767
+
768
+ A failure is **reported, not raised**: a refused key, an exhausted balance, a
769
+ url nothing serves all come back as `{ error, hint }` for the model to read and
770
+ act on. `web_read` reports per-url failures in `failed` alongside the pages that
771
+ did load, so one bad link does not lose the rest.
772
+
773
+ **Prompting for them.** Three lines earn their place in any prompt granting this
774
+ group:
775
+
776
+ - Search with a sentence, not keywords — the query is read by a model.
777
+ - Never quote an excerpt. `web_search` says which page to open; `web_read` says
778
+ what it contains.
779
+ - Say when the web is allowed to override what the model already believes, and
780
+ when it is not. Without that line, a retrieved page and a memorised fact carry
781
+ equal weight.
782
+
783
+ ### 3.9 Running it
784
+
785
+ `zen run` is what turns this folder into a running system. It reads the
786
+ directory, checks it, builds the workspace and sandbox tools against the
787
+ session's workspace, and starts the conversation:
788
+
789
+ ```
790
+ zen run open the entry agent on this project
791
+ zen run "what changed?" one shot; stdout is the answer
792
+ zen run --session <id> continue a session
793
+ zen run --workspace ./repo what the agent may read and write
794
+ zen run --model careful override the default model for this run
795
+ zen run --image <ref> override the sandbox image for this run
796
+ zen run --read-only withhold every tool that can write
797
+ ```
798
+
799
+ The project is read **once, up front**: a missing prompt, an unknown tool name,
800
+ a handoff to nobody all fail before the first call, with the offending key
801
+ named. Flags always win over the file — the repository states intent, the
802
+ invocation overrides it.
803
+
804
+ A session owns a workspace, a trajectory, memory and whatever the agent wrote,
805
+ under `sessions/`. None of it is source; none of it is committed.
806
+
807
+ ### 3.10 `.env`
808
+
809
+ ```
810
+ OPENAI_API_KEY=...
811
+ ANTHROPIC_API_KEY=...
812
+ GOOGLE_APPLICATION_CREDENTIALS=./.keys/vertex.json
813
+ EXA_API_KEY=...
814
+ ```
815
+
816
+ Never commit. Never inline a key into `agents.yaml` — use `${VAR}`. Never print a
817
+ key in a log line, a test fixture, or a chat message.
818
+
819
+ ---
820
+
821
+ ## 4. Writing prompts
822
+
823
+ ### 4.1 Composition order
824
+
825
+ ```
826
+ 1. INSTRUCTIONS.md (shared, once, all agents)
827
+ 2. agents/prompts/<n>.md (this agent)
828
+ 3. skill index (rendered by the runtime — do not hand-write it)
829
+ 4. preloaded skills (activated before the first call)
830
+ ```
831
+
832
+ Never duplicate `INSTRUCTIONS.md` content into an agent prompt; never hand-render
833
+ a list of skills into a prompt (the runtime does it, and a hand-written one goes
834
+ stale silently).
835
+
836
+ ### 4.2 Rules
837
+
838
+ 1. **Second person, imperative, present tense.** "Call `policy_lookup` with the
839
+ reference." Not "The agent should be able to look up policies."
840
+ 2. **Name things literally.** Tools in backticks by their exact name; agents by
841
+ their exact `name:`. The model matches strings.
842
+ 3. **State the boundary and the reason.** "Do not quote coverage rules — you do
843
+ not have them." A prohibition without a reason gets rationalised away.
844
+ 4. **Prefer prohibitions that are specific.** "Never say 'approved'" beats "be
845
+ careful about commitments".
846
+ 5. **Give the shape of the output, not a template to fill.** "Three short
847
+ paragraphs: what happened, what the policy says, what happens next."
848
+ 6. **State the failure path.** What to do when the tool errors, the reference is
849
+ missing, the case does not fit. Unstated failure modes get invented.
850
+ 7. **One instruction per line.** Contradictions become visible when they are
851
+ adjacent.
852
+ 8. **No meta-talk.** Do not explain the runtime, the trajectory, tokens, or that
853
+ it is an AI. That is context the model must pay for and cannot act on.
854
+ 9. **No hedging.** "Try to", "if possible", "generally" are read as permission
855
+ to skip.
856
+ 10. **Facts do not belong in prompts.** A number, a rate, a policy clause belongs
857
+ in a skill (changeable, versioned, loaded when relevant) or a tool
858
+ (authoritative, computed). A prompt that must be edited to change a fee is
859
+ mis-factored.
860
+
861
+ ### 4.3 Voice
862
+
863
+ Write like a competent colleague briefing a new hire on their first day: short
864
+ sentences, concrete nouns, the reason behind each rule. The model mirrors the
865
+ register it is given — a prompt written in bureaucratic hedging produces
866
+ bureaucratic hedging.
867
+
868
+ ### 4.4 Debugging a prompt
869
+
870
+ When behaviour is wrong, in this order:
871
+
872
+ 1. **Read the actual assembled prompt**, not the file. Use the inspect report.
873
+ 2. Is the instruction _present_? (Missed skill load, wrong agent, compaction.)
874
+ 3. Is it _contradicted_ by `INSTRUCTIONS.md` or a skill? Adjacent contradictions
875
+ win over distant ones; later text usually wins over earlier.
876
+ 4. Is it _specific enough to be checkable_? Rewrite as a testable assertion.
877
+ 5. Only then consider a stronger model.
878
+
879
+ Changing the model to fix an instruction-following bug hides the bug and pays for
880
+ it forever.
881
+
882
+ ---
883
+
884
+ ## 5. Context engineering
885
+
886
+ ### 5.1 Progressive disclosure
887
+
888
+ The decision for every piece of knowledge:
889
+
890
+ | Where | Cost | Use when |
891
+ | ------------------- | ----------------------- | ----------------------------------------------------- |
892
+ | `INSTRUCTIONS.md` | every call, every agent | true always, for everyone |
893
+ | agent prompt | every call, one agent | true always, for this job |
894
+ | **preloaded skill** | every call, one agent | always needed, but versioned/shared separately |
895
+ | **indexed skill** | one line until loaded | needed _sometimes_, model can tell when from one line |
896
+ | **searched skill** | nothing until searched | catalog too large to index (>~30 entries) |
897
+ | **tool** | schema only | needs live data, exact arithmetic, or a side effect |
898
+ | **memory** | recall block when hit | learned across runs, not authored |
899
+
900
+ ### 5.2 Skill bindings
901
+
902
+ ```yaml
903
+ agents:
904
+ - name: adjuster
905
+ skills:
906
+ provider: project # which catalog; defaults to the sole one
907
+ discovery: index # index | search | none
908
+ allow: [water_damage, storm_damage, house_style] # restrict the catalog
909
+ preload: [house_style] # active before the first call
910
+ maxIndexEntries: 20 # cap the rendered index
911
+ ```
912
+
913
+ - **`index`** — names and descriptions rendered into the system prompt. The
914
+ default and right answer for most catalogs.
915
+ - **`search`** — no index, only a `skill_search` tool. For catalogs too large to
916
+ render.
917
+ - **`none`** — preloads only.
918
+ - **`preload`** — for content there is no case for the model to decline: house
919
+ tone, a formatting contract. Making it choose is a wasted round trip.
920
+ Preloaded skills are filtered _out_ of the rendered index, and their activation
921
+ lands at the head of the transcript, inside the cached prefix.
922
+ - **`allow`** — how one catalog serves several agents without each seeing all of
923
+ it. A `preload` entry must also be in `allow`.
924
+
925
+ ### 5.3 Memory
926
+
927
+ Memory is the read-write twin of skills: written by the run, not authored. A
928
+ **scope** is a namespace string; agents bound to the same scope share what is
929
+ in it, and the default scope is the agent's own — so memory is private until
930
+ something says otherwise. Recall is automatic: matches for the current input
931
+ are injected before each call, which is what makes memory work without the
932
+ model remembering to look, and an agent with write access also gets tools to
933
+ search, add, update and delete entries.
934
+
935
+ It is a session-level facility rather than an `agents.yaml` key: `zen run`
936
+ binds each session's store under `sessions/`, so memory travels with the
937
+ session and is not part of this repository.
938
+
939
+ Do not treat memory as a database. It is for things learned that should
940
+ persist. Anything authoritative — a rate, a policy clause, a procedure — belongs
941
+ in a skill, where it is versioned and reviewable.
942
+
943
+ ### 5.4 Cache discipline
944
+
945
+ Rules that follow directly from "stable prefix = cache hit":
946
+
947
+ - Do not reorder `agents.yaml` for cosmetic reasons — tool order is prompt order.
948
+ - Do not put timestamps, run ids, or "today is …" in `INSTRUCTIONS.md` or a
949
+ prompt. A changing prefix is a permanent cache miss. Put volatile facts in a
950
+ tool result.
951
+ - Prefer `preload` over an instruction telling the model to load a skill first.
952
+ - Keep the volatile half of an instruction in the agent prompt and the stable
953
+ half in `INSTRUCTIONS.md`, not the reverse.
954
+
955
+ ---
956
+
957
+ ## 6. Organising agents
958
+
959
+ ### 6.1 When to split
960
+
961
+ Split into a second agent when at least one is true:
962
+
963
+ - The two jobs want **different models** (a cheap router, a careful writer).
964
+ - The two jobs want **different tools**, and giving both to one agent invites
965
+ misuse.
966
+ - The two jobs want **contradictory instructions** ("be exhaustive" vs "be brief").
967
+ - One job needs a **large knowledge slice** the other never touches.
968
+ - The boundary is a real **handoff in the business process** ("passed to an
969
+ adjuster").
970
+
971
+ Do **not** split because:
972
+
973
+ - The prompt got long — split it into skills instead.
974
+ - It feels tidier — every handoff costs a full re-render of the system prompt and
975
+ a fresh cache prefix.
976
+ - You want "a planner and an executor" with no distinct tools or models — that is
977
+ one agent with a numbered procedure.
978
+
979
+ **Start with one agent. Add the second when a specific case forces it.**
980
+
981
+ ### 6.2 Patterns
982
+
983
+ **Router + specialists.** A cheap, fast agent whose only job is classification
984
+ and handoff. Its prompt is short, it holds few tools, and it must be forbidden
985
+ from answering. Specialists never hand back to it.
986
+
987
+ ```yaml
988
+ agents:
989
+ - { name: intake, model: router, handoffs: [billing, technical, escalation] }
990
+ - { name: billing, model: balanced, skills: { allow: [refund_policy, invoicing] } }
991
+ - { name: technical, model: balanced, tools: [search_logs, restart_service] }
992
+ - { name: escalation, model: careful }
993
+ ```
994
+
995
+ **Pipeline.** Fixed stages, each handing to the next; only the last answers the
996
+ user. Encode the order in `handoffs:` so a stage cannot skip ahead.
997
+
998
+ **Fan-out / join.** For independent parallel work — ten regions, four review
999
+ lenses, six candidate suppliers — declare `fork:` on the agent that owns the
1000
+ work and let the model split it. See §6.4.
1001
+
1002
+ **Single agent + rich catalog.** One agent, `discovery: index`, twenty skills.
1003
+ Cheapest to run, cheapest to reason about, and correct far more often than the
1004
+ multi-agent instinct suggests.
1005
+
1006
+ ### 6.3 Handoffs
1007
+
1008
+ - `description:` on the target agent is what the model reads when deciding.
1009
+ Write it as a routing condition: _"Applies the written peril policies to a
1010
+ claim and explains the outcome."_ — not _"The adjuster agent."_
1011
+ - Handoffs are bare name strings; there is no per-edge configuration.
1012
+ - Self-handoff is a load error. Cycles are legal but usually a bug — a router in
1013
+ the `handoffs` of its own specialists produces ping-pong.
1014
+ - Handoff collapses history by policy: the receiving agent sees a selection, not
1015
+ the full transcript. Do not assume it saw a detail three turns back; if it
1016
+ matters, put it in the handoff.
1017
+
1018
+ ### 6.4 Forking (fan-out / join)
1019
+
1020
+ Forking is **opt-in per agent**. Without the key the agent is never offered the
1021
+ `fork` tool and cannot split, however obviously parallel the work looks:
1022
+
1023
+ ```yaml
1024
+ agents:
1025
+ - name: trunk
1026
+ fork: true # unrestricted: any agent, any number of branches
1027
+
1028
+ - name: sweep
1029
+ fork:
1030
+ agents: [prober] # every branch runs the specialist
1031
+ maxBranches: 6
1032
+ ```
1033
+
1034
+ | Field | Default | Meaning |
1035
+ | ------------- | -------------------- | ------------------------------------- |
1036
+ | `agents` | every declared agent | Which agents a branch may run |
1037
+ | `maxBranches` | unlimited | Cap on branches per call; minimum `2` |
1038
+
1039
+ The **model** decides the split: it names N branches, each with self-contained
1040
+ instructions. They run truly concurrently and rejoin as **one tool call and one
1041
+ tool result** in the parent's history — so N branches cost the parent
1042
+ O(N × summary), not O(N × full history).
1043
+
1044
+ Rules worth knowing before you write the key:
1045
+
1046
+ - `agents:` **may include the forking agent itself** — unlike `handoffs:`, that
1047
+ is not an error, and one role fanned out over ten items is the common shape.
1048
+ The list reaches the model as an `enum`, so a name outside it cannot even be
1049
+ decoded.
1050
+ - A fork always needs **at least two** branches. A one-branch call is refused
1051
+ with a message telling the model to do the work itself instead.
1052
+ - **Branches cannot talk to each other.** If branch B needs branch A's answer,
1053
+ it is a sequence, not a fork — keep it in one conversation.
1054
+ - Nesting is capped by the run's `maxForkDepth` (2 by default), so a branch may
1055
+ fork again but not without bound.
1056
+ - Fork vs handoff: a handoff is _one_ conversation changing owner; a fork is the
1057
+ _same_ question asked N times at once and merged.
1058
+
1059
+ Choose `context:` deliberately — the model sets it per call, so say in the
1060
+ agent's prompt which one this work wants:
1061
+
1062
+ - `inherit` — branch gets the full prefix. For work that depends on the case.
1063
+ - `compact` — prefix minus tool calls, tool results and recalls: _what was
1064
+ decided_, not the raw noise. **The default for wide fan-outs.**
1065
+ - `none` — system prompt plus instructions only. Cheapest; for independent
1066
+ lookups.
1067
+
1068
+ Declaring `fork:` only makes the tool available. Say in the agent's prompt when
1069
+ to reach for it, in the terms of this domain — _"When the request covers more
1070
+ than one region, fork one branch per region and merge their findings"_ — or a
1071
+ weaker model will work through the list serially and never call it.
1072
+
1073
+ ### 6.5 Termination
1074
+
1075
+ A turn ends when the model replies with no tool calls — that text is the answer.
1076
+ There is no turn limit, so an agent that must stop somewhere needs a prompt that
1077
+ says where: what "done" looks like, and what to do when it cannot get there.
1078
+
1079
+ So say in the prompt which handoff ends the turn, or what the final answer
1080
+ should contain. An agent with no stated finish keeps working the problem.
1081
+
1082
+ ---
1083
+
1084
+ ## 7. Model selection
1085
+
1086
+ ### 7.1 Tiers
1087
+
1088
+ Think in three tiers, and give them **role names, not vendor names** — so the
1089
+ mapping changes in one place.
1090
+
1091
+ | Alias | Role | Reasoning |
1092
+ | ---------- | ----------------------------------------------------------- | -------------- |
1093
+ | `router` | Classify, extract, route, validate. High volume, low stakes | minimal / none |
1094
+ | `balanced` | The default worker: apply written rules, use tools, answer | low / medium |
1095
+ | `careful` | Ambiguity, multi-step planning, anything a human will sign | high |
1096
+
1097
+ Add `writer` only if tone matters enough to justify a separate model.
1098
+
1099
+ ### 7.2 Worked example
1100
+
1101
+ ```yaml
1102
+ providers:
1103
+ # `vertex` needs no key: the GenAI SDK resolves Application Default
1104
+ # Credentials itself. Declared here only to pin the region.
1105
+ vertex-eu:
1106
+ kind: vertex
1107
+ location: europe-west4
1108
+ claude:
1109
+ kind: anthropic
1110
+ apiKey: ${ANTHROPIC_API_KEY}
1111
+
1112
+ models:
1113
+ router:
1114
+ provider: vertex-eu
1115
+ model: gemini-3.5-flash-lite
1116
+ thinkingLevel: minimal
1117
+ balanced:
1118
+ provider: vertex-eu
1119
+ model: gemini-3.5-flash
1120
+ thinkingLevel: high
1121
+ careful:
1122
+ provider: openai
1123
+ api: responses
1124
+ model: gpt-5.4-mini
1125
+ reasoningEffort: high
1126
+ reasoningSummary: auto # so the reasoning is visible while it works
1127
+ writer:
1128
+ provider: claude
1129
+ model: claude-sonnet-4-5
1130
+ maxTokens: 16000
1131
+
1132
+ model: balanced # fallback for agents that do not pin their own
1133
+
1134
+ agents:
1135
+ - { name: intake, model: router, handoffs: [adjuster] }
1136
+ - { name: adjuster, model: balanced, handoffs: [escalation] }
1137
+ - { name: escalation, model: careful }
1138
+ ```
1139
+
1140
+ ### 7.3 Shorthand
1141
+
1142
+ ```
1143
+ [provider[/api]:]model
1144
+ ```
1145
+
1146
+ `gpt-4o` · `openai:gpt-4o` · `openai/responses:o3` · `vertex:gemini-3.5-flash`
1147
+
1148
+ Only the **first** colon separates, so a fine-tuned id must name its provider:
1149
+ `openai:ft:gpt-4o:acme::a1b2`.
1150
+
1151
+ **Always write the prefix.** The first segment is a provider _name_, not a vendor
1152
+ hint — nothing reads `gemini-3.5-flash` and infers Google. An unprefixed id goes
1153
+ to the default provider, which is `openai` unless a top-level `provider:` says
1154
+ otherwise, so a bare `gemini-3.5-flash` asks OpenAI for a Google model and fails
1155
+ with `OPENAI_API_KEY is not set`. The message names the provider it resolved to;
1156
+ read it as "the prefix is missing", not "the key is missing".
1157
+
1158
+ Anything the shorthand cannot express (keys, base urls, `api:`, reasoning knobs)
1159
+ needs the object form. The object form does **not** re-parse a shorthand: its
1160
+ `model:` is the bare id and the provider goes in `provider:` beside it. Writing
1161
+ `model: openai:gpt-5.4-mini` there sends that whole string to the API.
1162
+
1163
+ Resolution order for any `model:` value: `zen run --model` → this file's
1164
+ `models:` → the shorthand parser. Two agents naming `balanced` share one model
1165
+ over one connection.
1166
+
1167
+ ### 7.4 Vendor knobs
1168
+
1169
+ | Field | Applies to | Notes |
1170
+ | ----------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------- |
1171
+ | `reasoningEffort` | openai, openrouter | Free string on purpose — the API is the authority on validity. On openai **needs `api: responses`** — §7.6 |
1172
+ | `reasoningSummary` | openai, openrouter | `auto` \| `concise` \| `detailed`. On openai **needs `api: responses`** — §7.6 |
1173
+ | `maxTokens` | anthropic, gemini, openrouter | Cap on **output** tokens, not context. Anthropic requires one (default 8192) |
1174
+ | `thinkingBudgetTokens` | anthropic | Extended thinking budget |
1175
+ | `thinkingBudget` | gemini 2.5 | Tokens: `0` off, `-1` auto |
1176
+ | `thinkingLevel` | gemini 3 | `minimal` \| `low` \| `medium` \| `high` |
1177
+ | `includeThoughts` | gemini | Thought summaries; default `true` |
1178
+ | `routing` / `fallbacks` | openrouter | Upstream provider preferences, and models to fall back to — §7.5 |
1179
+
1180
+ Knobs that do not apply to the chosen vendor are ignored, not rejected.
1181
+ `api:` exists only for the OpenAI protocol — naming it on a Gemini or Anthropic
1182
+ model is an error.
1183
+
1184
+ Each vendor's own SDK is used rather than its OpenAI-compatible endpoint, because
1185
+ those endpoints drop exactly what this runtime is built on: thinking budgets,
1186
+ thought signatures, cache accounting. `openai-compatible` is the shim kind — vLLM,
1187
+ a self-hosted gateway. `openrouter` used to be that shim with its base url
1188
+ (`https://openrouter.ai/api/v1`) and key env (`OPENROUTER_API_KEY`) filled in; it
1189
+ now has its own SDK, which is what makes provider routing and fallback chains
1190
+ available — §7.5.
1191
+
1192
+ ### 7.5 OpenRouter
1193
+
1194
+ A gateway: one key and one endpoint in front of several hundred models from
1195
+ every vendor. Useful when a project wants to compare families without holding
1196
+ four accounts, and when a cheap tier should be swappable by editing one id.
1197
+
1198
+ The whole declaration is the kind — `baseURL` and `OPENROUTER_API_KEY` are its
1199
+ defaults, and built-in kinds are usable as provider names, so a `providers:`
1200
+ entry is only worth writing when it adds something:
1201
+
1202
+ ```yaml
1203
+ agents:
1204
+ - name: triage
1205
+ model: openrouter:anthropic/claude-sonnet-4.5
1206
+ ```
1207
+
1208
+ **Model ids** are `vendor/model` and may carry a variant suffix after a colon —
1209
+ `:free`, `:nitro` (throughput-routed), `:floor` (price-routed), `:online` (web
1210
+ search). Both survive the shorthand, because only the _first_ colon separates:
1211
+
1212
+ | Ref | Provider | Model |
1213
+ | -------------------------------- | ------------ | --------------------- |
1214
+ | `openrouter:openai/gpt-5.4-nano` | `openrouter` | `openai/gpt-5.4-nano` |
1215
+ | `openrouter:z-ai/glm-5.2:free` | `openrouter` | `z-ai/glm-5.2:free` |
1216
+
1217
+ The `vendor/` prefix is part of the _id_, not a provider name: what precedes the
1218
+ first colon is the provider, and the `provider/api` slash is only read there.
1219
+
1220
+ **No api to choose.** This kind speaks one protocol, its own, so `api: responses`
1221
+ and `openrouter/responses:…` are both a load error ("has one api, so … means
1222
+ nothing here") rather than a 404 from the gateway at the first request. Reasoning
1223
+ arrives on the message and is read into `thinking` deltas; `reasoningEffort` and
1224
+ `reasoningSummary` are both forwarded, and the gateway maps effort onto whatever
1225
+ the destination model understands.
1226
+
1227
+ **Routing and fallbacks** are the reason this kind has an SDK. `routing` picks
1228
+ the upstream provider (OpenRouter's `provider` field, renamed because `provider:`
1229
+ already means the connection); `fallbacks` lists other _models_ to try when none
1230
+ can serve it (its `models` field). `allowFallbacks`, below, is a third thing
1231
+ again — whether the gateway may look past `order`:
1232
+
1233
+ ```yaml
1234
+ models:
1235
+ routed:
1236
+ provider: openrouter
1237
+ model: openai/gpt-5.4-nano
1238
+ routing:
1239
+ order: [azure, openai]
1240
+ requireParameters: true
1241
+ sort: throughput
1242
+ fallbacks: [anthropic/claude-sonnet-4.5]
1243
+ ```
1244
+
1245
+ `routing` belongs to a **model**, not to a `providers:` entry: it is chosen per
1246
+ request, not per connection, and the provider schema is strict, so writing it
1247
+ there is a load error.
1248
+
1249
+ | Key | Value |
1250
+ | ------------------- | ---------------------------------------------------------------------------- |
1251
+ | `order` | Providers to try first — a _preference_, not a restriction |
1252
+ | `only` / `ignore` | Restrict serving to, or away from, these |
1253
+ | `allowFallbacks` | May the gateway go beyond `order` — **on** unless set `false` |
1254
+ | `sort` | `price`, `throughput`, `latency`, `exacto` |
1255
+ | `requireParameters` | Skip a provider that would drop a parameter rather than serve it |
1256
+ | `dataCollection` | `allow` \| `deny` |
1257
+ | `quantizations` | `int4` `int8` `fp4` `mxfp4` `nvfp4` `fp6` `fp8` `mxfp8` `fp16` `bf16` `fp32` |
1258
+ | `zdr` | Zero-data-retention endpoints only |
1259
+
1260
+ `serviceTier` (`auto` \| `default` \| `fast` \| `flex` \| `priority` \| `scale`)
1261
+ sits alongside `routing`, not inside it.
1262
+
1263
+ **A typo in `order` is invisible.** Provider names and `sort` are free strings
1264
+ for the same reason `reasoningEffort` is — the gateway's list moves faster than a
1265
+ schema would — so nothing local rejects them, and since `allowFallbacks` defaults
1266
+ on, an unknown name is skipped and the request quietly succeeds somewhere else.
1267
+ What _is_ checked is checked by the API rather than at load: a bad `sort` or
1268
+ `quantizations` returns `400 provider.sort: Invalid input`. Use `only`, or
1269
+ `allowFallbacks: false`, when the constraint is meant to bind — an unroutable
1270
+ request is then a 404 instead of a silent reroute.
1271
+
1272
+ **Attribution** goes in `headers:`; there is no dedicated field because that one
1273
+ already means "sent on every request":
1274
+
1275
+ ```yaml
1276
+ providers:
1277
+ openrouter:
1278
+ kind: openrouter
1279
+ headers:
1280
+ HTTP-Referer: https://example.com
1281
+ X-Title: My Agent
1282
+ ```
1283
+
1284
+ **Check capabilities before pinning an id.** A gateway routes to whoever serves
1285
+ that model, so a request can fail on a capability rather than on the model
1286
+ existing (`404 No endpoints found that support image input`). The catalog is
1287
+ public and needs no key:
1288
+
1289
+ ```bash
1290
+ curl -s https://openrouter.ai/api/v1/models | jq -r '
1291
+ .data[] | select(.id == "z-ai/glm-5.2:free")
1292
+ | "modalities: \(.architecture.input_modalities | join("+"))",
1293
+ "params: \(.supported_parameters | join(","))"'
1294
+ ```
1295
+
1296
+ `input_modalities` decides whether images may be sent at all;
1297
+ `supported_parameters` decides whether `tools`, `tool_choice` and
1298
+ `reasoning_effort` are honoured. An agent with tools needs `tools` in that list.
1299
+
1300
+ **Not modelled yet:** `transforms`, `usage.include`, and per-call cost — the
1301
+ gateway reports a price on every response, but it is not surfaced in the token
1302
+ accounting. `plugins` exists in code only. Four `provider` fields the SDK accepts
1303
+ have no yaml spelling either — `maxPrice`, `preferredMaxLatency`,
1304
+ `preferredMinThroughput`, `enforceDistillableText` — and `sort` takes the string
1305
+ form only, not the `{ by, partition }` object. `models:` entries are strict, so
1306
+ writing any of these is a load error rather than a key that is silently dropped.
1307
+
1308
+ **`maxRetries` is honoured only as `0`.** This SDK takes a retry _strategy_, not
1309
+ a count, so `0` disables retries and any other number leaves the default backoff
1310
+ in place. `timeoutMs` and `headers` behave normally.
1311
+
1312
+ **Keys:** `zen key add openrouter` stores it under `OPENROUTER_API_KEY`.
1313
+
1314
+ ### 7.6 Turning reasoning on
1315
+
1316
+ Ask two separate questions: does the model **reason**, and does it **say what it
1317
+ reasoned**. They are different knobs, and the second is off by default on every
1318
+ vendor except Gemini — which is why a reasoning model can burn thousands of
1319
+ thinking tokens while the CLI shows no progress at all.
1320
+
1321
+ **OpenAI** — reasoning text only exists on the **responses** API, and only as a
1322
+ summary. `api: responses` is not optional here, and not only for visibility:
1323
+ chat completions is the default, and it **refuses `reasoningEffort` together with
1324
+ function tools** — `400 Function tools with reasoning_effort are not supported
1325
+ for <model> in /v1/chat/completions`. So any OpenAI agent that both reasons and
1326
+ holds tools — which is nearly all of them — must name the api.
1327
+
1328
+ ```yaml
1329
+ models:
1330
+ default:
1331
+ provider: openai
1332
+ api: responses # required — chat completions refuses tools + reasoning
1333
+ model: gpt-5.4-nano
1334
+ reasoningEffort: medium # how hard it thinks
1335
+ reasoningSummary: auto # whether you get to see it
1336
+ ```
1337
+
1338
+ **Anthropic** — `thinkingBudgetTokens` turns extended thinking on, and it is spent
1339
+ _out of_ `maxTokens`, so raise that too or the answer has no room left after the
1340
+ thinking. The runtime keeps 1024 tokens of headroom whatever you write, so a cap
1341
+ below the budget is corrected rather than rejected. Read §7.7 first: this
1342
+ combination is unsafe for tool-using agents.
1343
+
1344
+ ```yaml
1345
+ models:
1346
+ careful:
1347
+ provider: anthropic
1348
+ model: claude-sonnet-4-5
1349
+ maxTokens: 16000
1350
+ thinkingBudgetTokens: 8000
1351
+ ```
1352
+
1353
+ **Gemini** — thought summaries are on by default (`includeThoughts: true`); what
1354
+ varies is the budget. Gemini 3 takes `thinkingLevel`, Gemini 2.5 takes
1355
+ `thinkingBudget` in tokens.
1356
+
1357
+ ```yaml
1358
+ models:
1359
+ balanced:
1360
+ provider: vertex
1361
+ model: gemini-3.5-flash
1362
+ thinkingLevel: high
1363
+ ```
1364
+
1365
+ What this buys, in both views: `zen run` in the TUI streams the reasoning as a dim
1366
+ running tail above the answer, and the one-shot path prints it under `--live`.
1367
+ The full chain is kept in the trajectory either way and is in the inspect report,
1368
+ so turning summaries off costs visibility, not the audit trail.
1369
+
1370
+ The cost is real — a summary is extra output tokens on every call — so leave it
1371
+ on where someone is watching and reach for a cheaper tier before turning effort
1372
+ up (§7.8).
1373
+
1374
+ ### 7.7 Known traps
1375
+
1376
+ - **A model id with no provider prefix** — `gemini-3.5-flash` resolves to the
1377
+ default provider, not to Google, and the failure reads as a missing OpenAI key.
1378
+ Write `google:gemini-3.5-flash` — §7.3.
1379
+ - **OpenAI reasoning without `api: responses`** — chat completions rejects
1380
+ `reasoningEffort` alongside function tools with a `400`, and exposes no
1381
+ reasoning summary even without tools — §7.6.
1382
+ - **Anthropic + `thinkingBudgetTokens` + multi-turn tool use** — thinking-block
1383
+ signatures are not replayed, and the API rejects the follow-up. Leave extended
1384
+ thinking off for tool-using agents.
1385
+ - **`reasoningEffort: minimal`** is rejected by the `gpt-5.4-*` family; use `low`.
1386
+ - **Vertex** needs a project id, resolved from `project:` → `GOOGLE_CLOUD_PROJECT`
1387
+ → the `project_id` inside the key file named by `GOOGLE_APPLICATION_CREDENTIALS`.
1388
+ gcloud user credentials and metadata-server credentials carry no project id, so
1389
+ those deployments must set the variable.
1390
+ - **OpenRouter + a capability the route does not have** — a valid id and a valid
1391
+ key still fail at the first request (`404 No endpoints found that support image
1392
+ input`, or tools quietly unused). This is not a config error and `zen check`
1393
+ cannot see it: check the catalog (§7.5). Cheap `:free` tiers are the usual
1394
+ offenders — they are frequently text-only.
1395
+ - **Swapping an OpenRouter id is not a like-for-like change.** Two models behind
1396
+ one gateway differ in modalities, tool support and reasoning; re-run the case
1397
+ that uses the capability, not just any case.
1398
+ - **An `embeddings:` entry on an Anthropic provider** fails at load: that vendor
1399
+ publishes no embeddings API. Point it at another provider — §3.1.1.
1400
+ - **Changing `dimensions:` on an embedding invalidates every stored vector.**
1401
+ Widths are not comparable, so anything already indexed has to be re-embedded.
1402
+
1403
+ ### 7.8 How to choose, in practice
1404
+
1405
+ 1. Start every agent on `balanced`.
1406
+ 2. Demote to `router` any agent whose job is classification, extraction, or a
1407
+ fixed handoff — measure, do not guess.
1408
+ 3. Promote to `careful` only after seeing a concrete failure that a stronger
1409
+ model actually fixes.
1410
+ 4. Never fix an instruction-following bug with a model upgrade (§4.4).
1411
+ 5. Raise reasoning effort before switching model families: it is a smaller,
1412
+ reversible change.
1413
+
1414
+ ---
1415
+
1416
+ ## 8. Evaluating changes
1417
+
1418
+ There is no compiler for prose. Substitutes, in order of value:
1419
+
1420
+ 1. **`zen check`.** Most structural mistakes are load errors, and this is the
1421
+ fastest way to see all of them: it validates `agents.yaml`, checks that every
1422
+ prompt, skill and catalog it names is on disk, that hand-offs and tool
1423
+ selectors resolve, and that the models have credentials — without stopping at
1424
+ the first problem and without calling anything. `zen check --json` if you are
1425
+ parsing it.
1426
+ 2. **Run the case that motivated the change**, plus one that must _not_ change.
1427
+ 3. **Read the inspect report** — it shows the assembled prompt, every request and
1428
+ response, tool calls, skill activations and cost. Behaviour questions are
1429
+ answered there, not by re-reading the YAML.
1430
+ 4. **Re-run `zen check` after any rename.** It is what catches the handoff, the
1431
+ skill or the prompt path that a rename silently unlinked.
1432
+ 5. **Watch the token accounting.** A change that doubles prefix size is a
1433
+ regression even if the answer improved.
1434
+ 6. **After changing a model id, exercise the capability it was chosen for** —
1435
+ send an image, force a tool call, ask for reasoning. `zen check` proves the
1436
+ credential resolves, not that the route serves images or honours `tools`; on a
1437
+ gateway that gap is a request-time 404 (§7.5).
1438
+
1439
+ CLI (`zen --help` for the authoritative list): `zen init`, `zen run`, `zen check`,
1440
+ `zen inspect`, `zen models`, `zen key`, `zen list`. **stdout is the answer, stderr
1441
+ is the narration**; every command takes `--json`. Exit codes: `0` ok, `1` failed,
1442
+ `2` usage, `3` invalid project, `4` no usable credential.
1443
+
1444
+ ---
1445
+
1446
+ ## 9. Review checklist
1447
+
1448
+ Before finishing any change here:
1449
+
1450
+ **Structure**
1451
+
1452
+ - [ ] `agents.yaml` still loads; no unknown keys, no dangling names
1453
+ - [ ] Top-level `default:` names the entry agent explicitly
1454
+ - [ ] Every agent has a `description:` written as a routing condition
1455
+ - [ ] No self-handoff; no accidental cycle back to the router
1456
+ - [ ] Names match `^[a-z0-9]+(?:[-_][a-z0-9]+)*$`
1457
+ - [ ] An agent expected to fan out has `fork:`, and its prompt says when to use it
1458
+ - [ ] Comments explain the design, not the runtime or the key they sit above
1459
+
1460
+ **Prompts**
1461
+
1462
+ - [ ] `INSTRUCTIONS.md` carries the shared architecture and nothing agent-specific
1463
+ - [ ] Nothing duplicated between `INSTRUCTIONS.md` and an agent prompt
1464
+ - [ ] Every tool and agent referenced by its exact name
1465
+ - [ ] Failure paths stated for every instruction that can fail
1466
+ - [ ] No facts, rates or figures embedded in a prompt
1467
+ - [ ] No hedging, no meta-talk about the runtime
1468
+
1469
+ **Skills**
1470
+
1471
+ - [ ] Every skill has a `description` that says _when it is needed_
1472
+ - [ ] `preload` is reserved for content the model would never decline
1473
+ - [ ] `preload` entries also appear in `allow` where `allow` is used
1474
+ - [ ] Catalog >~30 entries → `discovery: search`
1475
+
1476
+ **Tools**
1477
+
1478
+ - [ ] Every agent holds the narrowest set its job needs
1479
+ - [ ] An agent that only reads is not holding `write_file`, `apply_patch`,
1480
+ `move_file` or `delete_file` — subtract them from `workspace:*`
1481
+ - [ ] `sandbox:*` is granted only where a shell is actually needed
1482
+ - [ ] `sandbox.persist: true`, unless a throwaway rootfs is wanted on purpose
1483
+ - [ ] The `sandbox:` image carries what the work needs, rather than the prompt
1484
+ installing it every run
1485
+ - [ ] `sandbox.env` lists names only, and nothing credential-shaped
1486
+ - [ ] `exa:*` is granted only where the live web is actually needed, and the
1487
+ prompt says when to trust it over what the model already believes
1488
+ - [ ] Every granted tool the prompt expects is named in that prompt
1489
+
1490
+ **Models**
1491
+
1492
+ - [ ] Aliases are role names, not vendor names
1493
+ - [ ] Vendor knobs valid for the chosen vendor (no `api:` on gemini/anthropic)
1494
+ - [ ] No agent silently on the fallback `model:` when it needed a specific tier
1495
+
1496
+ **Cache and cost**
1497
+
1498
+ - [ ] No timestamps or run-specific text in the prefix
1499
+ - [ ] Prefix growth is intentional and worth it
1500
+
1501
+ **Secrets**
1502
+
1503
+ - [ ] No key literal in YAML, prompt, skill or log — `${VAR}` only
1504
+ - [ ] `.env` is git-ignored
1505
+
1506
+ ---
1507
+
1508
+ ## 10. Where to change what
1509
+
1510
+ | Symptom | Change this |
1511
+ | ------------------------------------------ | ---------------------------------------------------------- |
1512
+ | Wrong tone, wrong format, wrong length | `INSTRUCTIONS.md` (all agents) or the agent prompt |
1513
+ | Says something forbidden | `INSTRUCTIONS.md` prohibition, stated specifically |
1514
+ | Ignores a rule that only applies sometimes | Move the rule into a skill with a sharp description |
1515
+ | Never loads the skill it should | The skill's `description`; or `preload` it |
1516
+ | Loads too much, answers slowly | `allow:`, `maxIndexEntries:`, or `discovery: search` |
1517
+ | Invents a number | A skill holding the figure, or a command that computes it |
1518
+ | Rewrites a whole file to change one line | A prompt line preferring `apply_patch` — §3.6 |
1519
+ | Edits files it should only be reading | Subtract the mutating tools, or `zen run --read-only` |
1520
+ | Cannot run the build or the tests | Grant `sandbox:*`; pick an `image:` that has the toolchain |
1521
+ | Installs the same packages on every run | `sandbox.persist: true`, or set `sandbox.image` — §3.7 |
1522
+ | Answers from stale knowledge of the world | Grant `web_search` + `web_read`, and say when — §3.8 |
1523
+ | Cites a page it only saw the excerpt of | A prompt line: `web_read` before quoting — §3.8 |
1524
+ | Every web call refuses | No Exa key: `zen key add exa` — `zen check` warns — §3.8 |
1525
+ | Answers instead of routing | Router prompt prohibition; check `handoffs:` |
1526
+ | Routes to the wrong specialist | The target agents' `description:` fields |
1527
+ | Loses a detail after a handoff | Say it in the handoff; check the collapse policy |
1528
+ | Works through N independent items serially | `fork:` on that agent, and a prompt line — §6.4 |
1529
+ | Forks when the steps actually depend | Prompt line: branches cannot see each other |
1530
+ | Slow and expensive on trivial cases | Demote that agent's model tier / reasoning effort |
1531
+ | Fails only on genuinely hard cases | Promote that agent's tier, or split the hard path out |
1532
+ | Shows no reasoning while it works | Turn summaries on for that model — §7.6 |
1533
+ | Forgets across conversations | Continue the session rather than starting a new one |
1534
+ | Breaks at load with a named path | Read the message — it names the exact key |
1535
+
1536
+ ---
1537
+
1538
+ ## 11. Anti-patterns
1539
+
1540
+ - **The mega-prompt.** 300 lines covering twelve scenarios. Split into skills.
1541
+ - **Agent sprawl.** Eight agents that share one model, one tool set and one
1542
+ prompt style. Collapse into one with a catalog.
1543
+ - **Facts in prompts.** A fee schedule inside `INSTRUCTIONS.md`. It cannot be
1544
+ versioned, cannot be shared, and is paid for on every call.
1545
+ - **Politeness padding.** "Please try your best to be helpful." Costs tokens,
1546
+ changes nothing.
1547
+ - **Commented implementation notes.** `agents.yaml` explaining how skill
1548
+ discovery or prompt caching works. That is this file's job — §3.1.
1549
+ - **Fixing prompts with models.** See §4.4.
1550
+ - **The chatty router.** A router that answers before handing off, because its
1551
+ prompt never forbade it.
1552
+ - **Ping-pong handoffs.** Specialists that hand back to the router, which hands
1553
+ back to a specialist.
1554
+ - **Forking a chain.** Branches never see each other, so a fork whose second
1555
+ branch needs the first branch's answer is a sequence wearing a fork's clothes.
1556
+ - **Installing the toolchain every run.** A prompt that begins with `apt-get
1557
+ install` is an `image:` that was never set — §3.7.
1558
+ - **Volatile prefix.** "Current date: …" in `INSTRUCTIONS.md`. Permanent cache
1559
+ miss.
1560
+
1561
+ ---
1562
+
1563
+ ## 12. Maintaining this file
1564
+
1565
+ This file is loaded on every request in this repository, so it is subject to its
1566
+ own rules: stable, factual, no hedging. Before adding a section, ask whether it
1567
+ is true of every task — if it is not, it is a skill under `.github/skills/`, or
1568
+ a prompt file under `.github/prompts/` if it is a procedure (§2.4).
1569
+
1570
+ Update it when:
1571
+
1572
+ - the layout changes (a new directory, a moved catalog)
1573
+ - a model alias is added, retired or repointed — §7.2 must match `agents.yaml`
1574
+ - a vendor trap is discovered — add it to §7.7
1575
+ - a recurring review comment appears twice — turn it into a checklist line in §9
1576
+ - a runtime capability is added — describe it here, or it will not be used
1577
+
1578
+ Do not let it grow without pruning. When a section is only true of one agent, it
1579
+ belongs in that agent's prompt, not here.