@zenera/cli 1.1.21 → 1.1.22

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 (49) hide show
  1. package/README.md +3 -3
  2. package/dist/banner.d.ts +33 -24
  3. package/dist/banner.js +25 -6
  4. package/dist/commands/check.js +1 -0
  5. package/dist/commands/init.js +1 -0
  6. package/dist/commands/inspect.js +1 -0
  7. package/dist/commands/key.js +1 -0
  8. package/dist/commands/list.js +1 -0
  9. package/dist/commands/memory.js +1 -0
  10. package/dist/commands/open.js +1 -0
  11. package/dist/commands/run.js +3 -0
  12. package/dist/commands/sandbox.js +1 -0
  13. package/dist/commands/version.js +1 -0
  14. package/dist/lib.d.ts +2 -1
  15. package/dist/lib.js +2 -1
  16. package/dist/meta.d.ts +1 -1
  17. package/dist/meta.js +5 -28
  18. package/dist/resolve.js +19 -12
  19. package/dist/sandbox.js +1 -0
  20. package/dist/scaffold.js +3 -1
  21. package/dist/term.d.ts +2 -0
  22. package/dist/term.js +2 -0
  23. package/dist/tui/markdown.d.ts +16 -0
  24. package/dist/tui/markdown.js +259 -0
  25. package/dist/tui/theme.js +1 -1
  26. package/dist/tui/wrap.d.ts +1 -1
  27. package/dist/validate.js +82 -3
  28. package/package.json +2 -2
  29. package/templates/editor/.github/copilot-instructions.md +62 -33
  30. package/templates/editor/.github/prompts/spec-sync-project.prompt.md +31 -1
  31. package/templates/editor/.github/skills/zen-cli/SKILL.md +15 -15
  32. package/templates/editor/.github/skills/zen-cli/references/projects.md +1 -0
  33. package/templates/editor/.github/skills/zen-cli/references/rag.md +37 -3
  34. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +30 -2
  35. package/templates/editor/.github/skills/zen-instructions/SKILL.md +232 -0
  36. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +554 -0
  37. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +56 -23
  38. package/templates/editor/.github/skills/zen-rag-schema/SKILL.md +55 -28
  39. package/templates/editor/.github/skills/zen-review/SKILL.md +17 -1
  40. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +257 -0
  41. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +15 -3
  42. package/templates/editor/.vscode/agents.schema.json +8 -3
  43. package/templates/project/SPECIFICATION.md.tmpl +2 -1
  44. package/templates/project/agents/files-instructions.md +86 -0
  45. package/templates/project/agents/fork-instructions.md +17 -0
  46. package/templates/project/agents/memory-instructions.md +37 -0
  47. package/templates/project/agents.yaml.tmpl +3 -3
  48. package/templates/project/gitignore +12 -1
  49. package/templates/project/scripts/_setup.sh +19 -4
@@ -0,0 +1,259 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Terminal Markdown Formatter
3
+ //
4
+ // Formats markdown text into styled ANSI terminal lines wrapped to a width.
5
+ //
6
+ // Consumes the block and span AST from `wrap.ts` and applies terminal styling
7
+ // from `term.ts` (bold, italic, green for code, underline for links, dimmed
8
+ // borders and markers). Line wrapping measures plain characters, so ANSI
9
+ // escape codes never cause lines to wrap early or misalign borders.
10
+ // ---------------------------------------------------------------------------
11
+ import { bold, cut, dim, green, italic, plain, styled, underline } from "../term.js";
12
+ import { blocksOf, gridOf, inlineOf } from "./wrap.js";
13
+ function renderPiece(piece) {
14
+ if (!styled()) {
15
+ return piece.text;
16
+ }
17
+ let t = piece.text;
18
+ if (piece.code) {
19
+ t = green(t);
20
+ }
21
+ if (piece.italic) {
22
+ t = italic(t);
23
+ }
24
+ if (piece.bold) {
25
+ t = bold(t);
26
+ }
27
+ if (piece.href) {
28
+ t = underline(t);
29
+ }
30
+ return t;
31
+ }
32
+ function sameStyle(a, b) {
33
+ return (!!a.bold === !!b.bold &&
34
+ !!a.italic === !!b.italic &&
35
+ !!a.code === !!b.code &&
36
+ a.href === b.href);
37
+ }
38
+ function mergePieces(pieces) {
39
+ const out = [];
40
+ for (const p of pieces) {
41
+ if (!p.text)
42
+ continue;
43
+ const last = out[out.length - 1];
44
+ if (last && sameStyle(last, p)) {
45
+ last.text += p.text;
46
+ }
47
+ else {
48
+ out.push({ ...p });
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+ function wordsOf(spans) {
54
+ const words = [];
55
+ let currentPieces = [];
56
+ let currentLen = 0;
57
+ for (const span of spans) {
58
+ const style = {
59
+ ...(span.bold ? { bold: true } : {}),
60
+ ...(span.italic ? { italic: true } : {}),
61
+ ...(span.code ? { code: true } : {}),
62
+ ...(span.href ? { href: span.href } : {}),
63
+ };
64
+ const parts = span.text.split(/(\s+)/);
65
+ for (const part of parts) {
66
+ if (!part)
67
+ continue;
68
+ if (/^\s+$/.test(part)) {
69
+ if (currentPieces.length > 0) {
70
+ words.push({ pieces: currentPieces, length: currentLen });
71
+ currentPieces = [];
72
+ currentLen = 0;
73
+ }
74
+ }
75
+ else {
76
+ currentPieces.push({ text: part, ...style });
77
+ currentLen += part.length;
78
+ }
79
+ }
80
+ }
81
+ if (currentPieces.length > 0) {
82
+ words.push({ pieces: currentPieces, length: currentLen });
83
+ }
84
+ return words;
85
+ }
86
+ function sliceWord(word, at) {
87
+ const leftPieces = [];
88
+ const rightPieces = [];
89
+ let taken = 0;
90
+ for (const p of word.pieces) {
91
+ if (taken + p.text.length <= at) {
92
+ leftPieces.push(p);
93
+ taken += p.text.length;
94
+ }
95
+ else if (taken < at) {
96
+ const cutIndex = at - taken;
97
+ leftPieces.push({ ...p, text: p.text.slice(0, cutIndex) });
98
+ rightPieces.push({ ...p, text: p.text.slice(cutIndex) });
99
+ taken = at;
100
+ }
101
+ else {
102
+ rightPieces.push(p);
103
+ }
104
+ }
105
+ return [
106
+ { pieces: leftPieces, length: at },
107
+ { pieces: rightPieces, length: word.length - at },
108
+ ];
109
+ }
110
+ export function wrapSpans(spans, width, prefix = '', subsequentPrefix = '') {
111
+ const w = Math.max(1, width);
112
+ const words = wordsOf(spans);
113
+ if (words.length === 0) {
114
+ return prefix ? [prefix] : [''];
115
+ }
116
+ const lines = [];
117
+ let currentWords = [];
118
+ const prefixLen = plain(prefix).length;
119
+ const subLen = plain(subsequentPrefix).length;
120
+ let currentLen = prefixLen;
121
+ const flush = () => {
122
+ if (currentWords.length === 0)
123
+ return;
124
+ const linePieces = [];
125
+ for (let i = 0; i < currentWords.length; i++) {
126
+ const word = currentWords[i];
127
+ if (i > 0) {
128
+ const prev = currentWords[i - 1];
129
+ const prevPiece = prev.pieces[prev.pieces.length - 1];
130
+ const nextPiece = word.pieces[0];
131
+ const sharedStyle = prevPiece && nextPiece && sameStyle(prevPiece, nextPiece)
132
+ ? { ...prevPiece, text: ' ' }
133
+ : { text: ' ' };
134
+ linePieces.push(sharedStyle);
135
+ }
136
+ linePieces.push(...word.pieces);
137
+ }
138
+ const p = lines.length === 0 ? prefix : subsequentPrefix;
139
+ lines.push(p + mergePieces(linePieces).map(renderPiece).join(''));
140
+ currentWords = [];
141
+ currentLen = subLen;
142
+ };
143
+ for (let word of words) {
144
+ const available = lines.length === 0 ? w - prefixLen : w - subLen;
145
+ while (word.length > available && available > 0) {
146
+ if (currentWords.length > 0) {
147
+ flush();
148
+ }
149
+ const [head, tail] = sliceWord(word, available);
150
+ const p = lines.length === 0 ? prefix : subsequentPrefix;
151
+ lines.push(p + mergePieces(head.pieces).map(renderPiece).join(''));
152
+ word = tail;
153
+ currentLen = subLen;
154
+ }
155
+ const spaceNeeded = currentWords.length > 0 ? 1 : 0;
156
+ if (currentLen + spaceNeeded + word.length <= w) {
157
+ currentWords.push(word);
158
+ currentLen += spaceNeeded + word.length;
159
+ }
160
+ else {
161
+ flush();
162
+ currentWords.push(word);
163
+ const p = lines.length === 0 ? prefix : subsequentPrefix;
164
+ currentLen = plain(p).length + word.length;
165
+ }
166
+ }
167
+ flush();
168
+ return lines;
169
+ }
170
+ /**
171
+ * Render a sequence of AST `Block`s into styled terminal lines wrapped to `width`.
172
+ */
173
+ export function formatBlocks(blocks, width = 80) {
174
+ const lines = [];
175
+ let previous;
176
+ for (const block of blocks) {
177
+ if (previous && !(block.kind === 'item' && previous.kind === 'item')) {
178
+ lines.push('');
179
+ }
180
+ previous = block;
181
+ if (block.kind === 'code') {
182
+ lines.push(dim(`\u250c\u2500${block.title ? ` ${block.title}` : ''}`));
183
+ lines.push(...(block.lines ?? []).map((l) => `${dim('\u2502')} ${cut(l, Math.max(0, width - 2))}`));
184
+ lines.push(dim('\u2514\u2500'));
185
+ }
186
+ else if (block.kind === 'table') {
187
+ for (const [j, row] of gridOf(block).entries()) {
188
+ let formatted;
189
+ if (block.align !== undefined && j === 0) {
190
+ formatted = row
191
+ .map((s) => s.text === '| ' || s.text === ' | ' || s.text === ' |'
192
+ ? dim(s.text)
193
+ : renderPiece({ ...s, bold: true }))
194
+ .join('');
195
+ }
196
+ else if (block.align !== undefined && j === 1) {
197
+ formatted = dim(row.map((s) => s.text).join(''));
198
+ }
199
+ else {
200
+ formatted = row
201
+ .map((s) => s.text === '| ' || s.text === ' | ' || s.text === ' |'
202
+ ? dim(s.text)
203
+ : renderPiece(s))
204
+ .join('');
205
+ }
206
+ lines.push(cut(formatted, width));
207
+ }
208
+ }
209
+ else if (block.kind === 'heading') {
210
+ const headingSpans = (block.spans ?? []).map((s) => ({ ...s, bold: true }));
211
+ lines.push(...wrapSpans(headingSpans, width));
212
+ }
213
+ else if (block.kind === 'item') {
214
+ const indentLevel = block.level ?? 0;
215
+ const marker = block.marker ?? '\u2022';
216
+ const leadSpaces = ' '.repeat(indentLevel);
217
+ const prefix = `${leadSpaces}${dim(marker)} `;
218
+ const subPrefix = `${leadSpaces}${' '.repeat(marker.length + 1)}`;
219
+ lines.push(...wrapSpans(block.spans ?? [], width, prefix, subPrefix));
220
+ }
221
+ else if (block.kind === 'quote') {
222
+ const quoteSpans = (block.spans ?? []).map((s) => ({ ...s, italic: true }));
223
+ lines.push(...wrapSpans(quoteSpans, width, `${dim('\u2502')} `, `${dim('\u2502')} `));
224
+ }
225
+ else if (block.kind === 'rule') {
226
+ lines.push(dim('\u2500'.repeat(width)));
227
+ }
228
+ else if (block.lines) {
229
+ lines.push(...block.lines.map((l) => cut(l, width)));
230
+ }
231
+ else {
232
+ lines.push(...wrapSpans(block.spans ?? [], width));
233
+ }
234
+ }
235
+ return lines;
236
+ }
237
+ /**
238
+ * Format markdown text into styled ANSI terminal lines wrapped to `width`.
239
+ */
240
+ export function formatMarkdown(text, width = 80) {
241
+ return formatBlocks(blocksOf(text), width);
242
+ }
243
+ /**
244
+ * Format inline markdown text (bold, italic, code, links) into a styled ANSI string.
245
+ * Markers like backticks and asterisks are resolved and removed.
246
+ */
247
+ export function formatInline(text) {
248
+ const spans = inlineOf(text);
249
+ return mergePieces(spans.map((s) => ({
250
+ text: s.text,
251
+ ...(s.bold ? { bold: true } : {}),
252
+ ...(s.italic ? { italic: true } : {}),
253
+ ...(s.code ? { code: true } : {}),
254
+ ...(s.href ? { href: s.href } : {}),
255
+ })))
256
+ .map(renderPiece)
257
+ .join('');
258
+ }
259
+ //# sourceMappingURL=markdown.js.map
package/dist/tui/theme.js CHANGED
@@ -168,7 +168,7 @@ async function queryBackground(timeoutMs = 300) {
168
168
  stdout.write(QUERY);
169
169
  });
170
170
  }
171
- /** Components come back as 1–4 hex digits, so each is scaled by its own width. */
171
+ /** Components come back as 1-4 hex digits, so each is scaled by its own width. */
172
172
  function appearanceOf(r, g, b) {
173
173
  const channel = (hex) => parseInt(hex, 16) / (16 ** hex.length - 1);
174
174
  const luminance = 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
@@ -154,7 +154,7 @@ export type Align = 'left' | 'right' | 'center';
154
154
  */
155
155
  export interface Block {
156
156
  kind: BlockKind;
157
- /** `heading`: its depth, 1–6. `item`: how deep it is nested. */
157
+ /** `heading`: its depth, 1-6. `item`: how deep it is nested. */
158
158
  level?: number;
159
159
  /** the fence's info string, when it had one */
160
160
  title?: string;
package/dist/validate.js CHANGED
@@ -15,24 +15,87 @@ const CONFIG_NAMES = ['agents.yaml', 'agents.yml', 'agents/agents.yaml', 'agents
15
15
  const AGENTS_DIR = 'agents';
16
16
  const HOUSE_RULES = 'agents/instructions.md';
17
17
  const INSTRUCTIONS_SUFFIX = '-instructions.md';
18
+ /** How file tools are used; without it the model has schemas only. */
19
+ const FILE_RULES = 'agents/files-instructions.md';
18
20
  /** The only place memory is explained to a model; the runtime composes none of it. */
19
21
  const MEMORY_RULES = 'agents/memory-instructions.md';
20
22
  /** The only place forking is explained to a model; the same holds. */
21
23
  const FORK_RULES = 'agents/fork-instructions.md';
22
24
  /** The conditioned documents `zen` ships, which land whether or not they apply. */
23
- const OURS = new Set([MEMORY_RULES, FORK_RULES]);
25
+ const OURS = new Set([FILE_RULES, MEMORY_RULES, FORK_RULES]);
24
26
  const PROMPTS_DIR = 'agents/prompts';
25
27
  const SKILLS_DIR = 'agents/skills';
26
28
  const SKILL_FILE = 'SKILL.md';
27
29
  const ASSETS_DIR = 'assets';
28
30
  /** What `allow:` and `preload:` accept, so a skill outside it cannot be named. */
29
31
  const REFERABLE = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
32
+ const FILE_TOOLS = [
33
+ 'read_file',
34
+ 'list_dir',
35
+ 'find_files',
36
+ 'write_file',
37
+ 'apply_patch',
38
+ 'move_file',
39
+ 'delete_file',
40
+ ];
41
+ const FILE_WRITE_TOOLS = new Set(['write_file', 'apply_patch', 'move_file', 'delete_file']);
42
+ function activeFileTools(selectors) {
43
+ const active = new Set();
44
+ if (!selectors?.length) {
45
+ return active;
46
+ }
47
+ for (const raw of selectors) {
48
+ const drop = raw.startsWith('-');
49
+ const selector = (drop ? raw.slice(1) : raw).trim();
50
+ if (selector === '*' || selector === 'files:*' || selector === 'workspace:*') {
51
+ for (const t of FILE_TOOLS) {
52
+ if (drop) {
53
+ active.delete(t);
54
+ }
55
+ else {
56
+ active.add(t);
57
+ }
58
+ }
59
+ continue;
60
+ }
61
+ const colon = selector.lastIndexOf(':');
62
+ const name = colon >= 0 ? selector.slice(colon + 1) : selector;
63
+ const group = colon >= 0 ? selector.slice(0, colon) : undefined;
64
+ if (FILE_TOOLS.includes(name)) {
65
+ if (!group || group === 'files' || group === 'workspace') {
66
+ if (drop) {
67
+ active.delete(name);
68
+ }
69
+ else {
70
+ active.add(name);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return active;
76
+ }
77
+ function hasFileTools(spec) {
78
+ return activeFileTools(spec.tools).size > 0;
79
+ }
80
+ function hasFileWriteTools(spec) {
81
+ const active = activeFileTools(spec.tools);
82
+ for (const t of FILE_WRITE_TOOLS) {
83
+ if (active.has(t)) {
84
+ return true;
85
+ }
86
+ }
87
+ return false;
88
+ }
30
89
  /** What `requires:` may name. Mirrors `CAPABILITIES` in the loader. */
31
90
  const CAPABILITIES = {
32
91
  fork: (spec) => Boolean(spec.fork),
33
92
  memory: (spec) => Boolean(spec.memory),
34
93
  'memory-write': (spec) => spec.memory?.access === 'read-write' || spec.memory?.access === 'full',
35
94
  'memory-forget': (spec) => spec.memory?.access === 'full',
95
+ files: (spec) => hasFileTools(spec),
96
+ 'files-write': (spec) => hasFileWriteTools(spec),
97
+ workspace: (spec) => hasFileTools(spec),
98
+ 'workspace-write': (spec) => hasFileWriteTools(spec),
36
99
  };
37
100
  /**
38
101
  * Mirrors `readHouseRules` in the loader, in the same order:
@@ -276,7 +339,23 @@ export async function validateProject(opts) {
276
339
  });
277
340
  }
278
341
  }
279
- // Present is not the same as current. These three files are copies of ours
342
+ // The same again for file operations: the file tools need instructions on
343
+ // discovery, safe reading, and patching. Without them the tools are granted
344
+ // with only their schemas to explain them.
345
+ if (config.agents.some((a) => hasFileTools(a))) {
346
+ const path = join(root, FILE_RULES);
347
+ if (!existsSync(path) || empty(path)) {
348
+ add({
349
+ severity: 'error',
350
+ code: 'files.uninstructed',
351
+ where: FILE_RULES,
352
+ message: 'an agent has file tools, but nothing tells it how to use them — ' +
353
+ 'the tools are granted with only their schemas to explain them',
354
+ fix: 'zen check --fix, which writes the current rules back',
355
+ });
356
+ }
357
+ }
358
+ // Present is not the same as current. These files are copies of ours
280
359
  // and nobody is meant to be maintaining them, so bytes that differ are an
281
360
  // older `zen`'s — prose about a runtime that has since moved, which reads
282
361
  // exactly as authoritative as the version that is true. The frontmatter
@@ -605,7 +684,7 @@ function checkAgent(root, config, spec, entry, available, rules, record, add) {
605
684
  try {
606
685
  tools = selectTools(available, [...selectors], {
607
686
  where: `${where}.tools`,
608
- hint: 'this CLI provides the workspace and sandbox groups',
687
+ hint: 'this CLI provides the files and sandbox groups',
609
688
  }).map((t) => t.name);
610
689
  }
611
690
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/cli",
3
- "version": "1.1.21",
3
+ "version": "1.1.22",
4
4
  "description": "Command-line front end for @zenera/neo: agentic projects you can run, share and commit.",
5
5
  "keywords": [
6
6
  "agents",
@@ -51,7 +51,7 @@
51
51
  "@inkjs/ui": "^2.0.0",
52
52
  "ink": "^7.1.1",
53
53
  "react": "^19.2.8",
54
- "@zenera/neo": "^1.1.21",
54
+ "@zenera/neo": "^1.1.22",
55
55
  "@anthropic-ai/sdk": "^0.120.0",
56
56
  "@google/genai": "^2.18.0",
57
57
  "@openrouter/sdk": "^1.2.80",
@@ -45,12 +45,22 @@ not reconstruct `zen` arguments from memory or call `--help` to discover them.
45
45
  **Every runtime capability a project turns on carries obligations that are not
46
46
  visible in `agents.yaml` and that `zen check` does not report.** Before
47
47
  reviewing or changing a project, list the capabilities it has turned on and load
48
- the editor skill for each one - `zen-memory` for a `memory:` block, `zen-rag-
49
- schema` or `zen-rag-docs` for an index, `zen-cli` always. A capability that is
50
- already configured and already passing `zen check` is exactly the case that
51
- looks finished and is not: the obligation lives in the skill, so a review that
52
- never opens the skill cannot find what is missing. This applies whatever
53
- prompted the change - a task scoped to `SPECIFICATION.md` does not narrow it.
48
+ the editor skill for each one - `zen-memory` for a `memory:` block,
49
+ `zen-sandbox` for sandbox configuration or shell tools, `zen-rag-schema` or
50
+ `zen-rag-docs` for an index, `zen-cli` always. A capability that is already
51
+ configured and already passing `zen check` is exactly the case that looks
52
+ finished and is not: the obligation lives in the skill, so a review that never
53
+ opens the skill cannot find what is missing. This applies whatever prompted the
54
+ change - a task scoped to `SPECIFICATION.md` does not narrow it.
55
+
56
+ **Load `zen-instructions` before writing, editing, reviewing or deleting any
57
+ `agents/*instructions.md`.** They are the house rules, prepended to every
58
+ agent's prompt, and three of them are `zen`'s own copies rather than the
59
+ project's: an edit made inside one is lost at the next `zen check --fix`, `zen
60
+ init` or `zen open`, with no diff to show for it. The skill is what says which
61
+ three those are, where a project's own rules on the same subjects go instead,
62
+ how `requires:` conditions a document, and what filename order decides. None of
63
+ that is visible in the file being edited.
54
64
 
55
65
  First determine whether the target project uses RAG: a `zen rag` command in an
56
66
  agent prompt or skill, RAG tools supplied by its host, or a documentation or
@@ -222,6 +232,7 @@ my-project/
222
232
  ├── agents.yaml who exists, what they may reach for
223
233
  ├── agents/
224
234
  │ ├── instructions.md house rules, prepended to every agent
235
+ │ ├── files-instructions.md and more - how files are handled
225
236
  │ ├── fork-instructions.md more of them - any `<topic>-instructions.md`
226
237
  │ ├── memory-instructions.md and more - how the graph is used
227
238
  │ ├── tools-instructions.md and more - how tools are called
@@ -631,9 +642,19 @@ follow. So:
631
642
  - Anything only one agent needs goes in that agent's prompt instead. If you find
632
643
  yourself writing "if you are the router…", you are in the wrong file.
633
644
 
634
- Target 20–60 lines each. If one exceeds ~100, split the stable half out into a
645
+ Target 20-60 lines each. If one exceeds ~100, split the stable half out into a
635
646
  preloaded skill.
636
647
 
648
+ Four of these documents are **`zen`'s, not the project's** -
649
+ `tools-instructions.md`, `files-instructions.md`, `memory-instructions.md` and
650
+ `fork-instructions.md`. They are replaced by `zen check --fix`, and a copy that
651
+ differs is reported as `rules.stale`. This project's own rules on any of those
652
+ subjects go in a topic file beside them - `agents/files-policy-instructions.md`,
653
+ `agents/memory-policy-instructions.md` and the like, carrying the same
654
+ `requires:`. The `zen-instructions` skill is the full treatment: the two owners,
655
+ the naming and ordering that keeps a policy after the rules it qualifies, and
656
+ every finding these files can produce. Load it before touching one.
657
+
637
658
  ### 3.3 `agents/prompts/<name>.md`
638
659
 
639
660
  Plain Markdown, no frontmatter. This is the agent's _job description_, appended
@@ -644,7 +665,7 @@ Structure that works:
644
665
  ```markdown
645
666
  <one sentence: who this agent is and what it owns>
646
667
 
647
- <what it does, as a short numbered procedure or 3–5 rules>
668
+ <what it does, as a short numbered procedure or 3-5 rules>
648
669
 
649
670
  <what it must NOT do - especially the neighbouring agent's job>
650
671
 
@@ -677,7 +698,7 @@ already did, §3.2), never repeats one of them, and never describes the runtime.
677
698
  Every line should be an instruction the model can act on or a boundary it can
678
699
  check itself against.
679
700
 
680
- Target 10–40 lines. A 200-line prompt is a skill catalog that has not been split
701
+ Target 10-40 lines. A 200-line prompt is a skill catalog that has not been split
681
702
  yet.
682
703
 
683
704
  ### 3.4 Skills
@@ -798,21 +819,21 @@ Three rules follow:
798
819
 
799
820
  - **Grant the narrowest set the job needs.** An agent that only reviews should
800
821
  not be holding the tools that overwrite files.
801
- - **Grant by group, then subtract.** Write `workspace:*` and take away what this
802
- agent must not have - not a hand-picked list of names. The tools in a group are
803
- designed to be used together, and a list assembled one name at a time leaves out
804
- the companion the model reaches for next: `write_file` without `apply_patch`
805
- turns every one-line edit into a whole-file rewrite, and `read_file` without
806
- `list_dir` and `find_files` leaves it guessing at paths. A group also tracks the
807
- runtime - a tool added to it later reaches the agent, where an enumerated list
808
- silently stays behind.
822
+ - **Grant by group, then subtract.** Write `files:*` (or `workspace:*`) and take
823
+ away what this agent must not have - not a hand-picked list of names. The tools
824
+ in a group are designed to be used together, and a list assembled one name at
825
+ a time leaves out the companion the model reaches for next: `write_file`
826
+ without `apply_patch` turns every one-line edit into a whole-file rewrite, and
827
+ `read_file` without `list_dir` and `find_files` leaves it guessing at paths. A
828
+ group also tracks the runtime - a tool added to it later reaches the agent,
829
+ where an enumerated list silently stays behind.
809
830
  - **Say in the prompt when to reach for what.** A granted tool the prompt never
810
831
  mentions is used at the model's discretion, which is not the same as never.
811
832
 
812
833
  Skills can own tools too - `tools:` in a skill's frontmatter (§3.4) names tools
813
834
  that refuse to run until that skill is active.
814
835
 
815
- ### 3.6 The workspace tools (`workspace:*`)
836
+ ### 3.6 The file tools (`files:*`, alias `workspace:*`)
816
837
 
817
838
  `zen run` builds this set for you, rooted at the session's workspace directory.
818
839
  Nothing else reaches the file system, so an agent whose `tools:` does not name
@@ -841,27 +862,27 @@ container involved there is no second name and everything stays relative.
841
862
 
842
863
  **Selecting them.** A `tools:` entry is a selector, not only a name:
843
864
 
844
- | Selector | Selects |
845
- | --------------------- | ------------------------------------------------- |
846
- | `read_file` | that one tool |
847
- | `workspace:read_file` | the same tool, written out in full |
848
- | `workspace:*` | every tool in the group |
849
- | `'*'` | every tool the runtime provides |
850
- | `-<any>` | removes what it matches from the selection so far |
865
+ | Selector | Selects |
866
+ | ------------------------------- | ------------------------------------------------- |
867
+ | `read_file` | that one tool |
868
+ | `files:read_file` | the same tool, written out in full |
869
+ | `files:*` (alias `workspace:*`) | every tool in the group |
870
+ | `'*'` | every tool the runtime provides |
871
+ | `-<any>` | removes what it matches from the selection so far |
851
872
 
852
873
  ```yaml
853
874
  agents:
854
875
  - name: editor
855
- tools: [workspace:*]
876
+ tools: [files:*]
856
877
 
857
878
  - name: reviewer
858
879
  # Everything except the four that can change something.
859
- tools: [workspace:*, -write_file, -apply_patch, -move_file, -delete_file]
880
+ tools: [files:*, -write_file, -apply_patch, -move_file, -delete_file]
860
881
  ```
861
882
 
862
883
  Selectors apply in the order written, so a `-` line reads as an exception to the
863
884
  line above it. Quote a lone `'*'`: unquoted, YAML reads it as an alias and
864
- refuses the file. `workspace:*` needs no quoting. There is no name globbing -
885
+ refuses the file. `files:*` (and `workspace:*`) needs no quoting. There is no name globbing -
865
886
  `read_*` is an unknown tool, because a selector should track a declared set, not
866
887
  a naming habit.
867
888
 
@@ -983,12 +1004,12 @@ sandbox:
983
1004
 
984
1005
  agents:
985
1006
  - name: builder
986
- tools: [workspace:*, sandbox:*]
1007
+ tools: [files:*, sandbox:*]
987
1008
  sandbox:
988
1009
  image: docker.io/library/node:22-bookworm-slim
989
1010
  memory: 8192
990
1011
  - name: analyst
991
- tools: [workspace:*, sandbox:*] # shares the project's container
1012
+ tools: [files:*, sandbox:*] # shares the project's container
992
1013
  ```
993
1014
 
994
1015
  **Write `persist: true` unless you have a reason not to.** By default the
@@ -1053,7 +1074,7 @@ over several pages, and wastes it when a specific document is wanted.
1053
1074
  ```yaml
1054
1075
  agents:
1055
1076
  - name: researcher
1056
- tools: [exa:*, workspace:*]
1077
+ tools: [exa:*, files:*]
1057
1078
 
1058
1079
  - name: fact-checker
1059
1080
  # Find and read, but never let a model on the far side do the reasoning.
@@ -1839,6 +1860,9 @@ input`, or tools quietly unused). This is not a config error and `zen check`
1839
1860
  publishes no embeddings API. Point it at another provider - §3.1.1.
1840
1861
  - **Changing `dimensions:` on an embedding invalidates every stored vector.**
1841
1862
  Widths are not comparable, so anything already indexed has to be re-embedded.
1863
+ For a `zen rag` index that is `zen rag <subject> restore --dimensions <n>`,
1864
+ which rebuilds from the copies the index already holds rather than from the
1865
+ documents; the same command with `--embedding <ref>` moves it to another model.
1842
1866
 
1843
1867
  ### 7.8 How to choose, in practice
1844
1868
 
@@ -1940,9 +1964,13 @@ Before finishing any change here:
1940
1964
  - [ ] Nothing duplicated between a house-rules file and an agent prompt
1941
1965
  - [ ] Each `agents/<topic>-instructions.md` is about one capability, and is true
1942
1966
  of every agent it reaches - `requires:` if it is about one of them
1967
+ - [ ] If any agent has file tools, `agents/files-instructions.md` is present - §3.6
1943
1968
  - [ ] If any agent has `fork:`, `agents/fork-instructions.md` is present - §6.4
1944
- - [ ] No `rules.stale`: the three files that are `zen`'s rather than yours say
1969
+ - [ ] No `rules.stale`: the four files that are `zen`'s rather than yours say
1945
1970
  what this version of the runtime does, and `zen check --fix` replaces them
1971
+ - [ ] Any change to an `agents/*instructions.md` was made with the
1972
+ `zen-instructions` skill loaded, and nothing of this project's was written
1973
+ into one of `zen`'s - §3.2
1946
1974
  - [ ] Every tool and agent referenced by its exact name
1947
1975
  - [ ] Failure paths stated for every instruction that can fail
1948
1976
  - [ ] No facts, rates or figures embedded in a prompt
@@ -1984,7 +2012,7 @@ candidates; these are the judgements to make about each one)**
1984
2012
  - [ ] No agent holds `write_file` without `apply_patch`, or `read_file` without
1985
2013
  `list_dir` and `find_files`
1986
2014
  - [ ] An agent that only reads is not holding `write_file`, `apply_patch`,
1987
- `move_file` or `delete_file` - subtract them from `workspace:*`
2015
+ `move_file` or `delete_file` - subtract them from `files:*` (or `workspace:*`)
1988
2016
  - [ ] `sandbox:*` is granted only where a shell is actually needed
1989
2017
  - [ ] `sandbox.persist: true`, unless a throwaway rootfs is wanted on purpose
1990
2018
  - [ ] The `sandbox:` image carries what the work needs, rather than the prompt
@@ -2056,6 +2084,7 @@ candidates; these are the judgements to make about each one)**
2056
2084
  | Works through N independent items serially | `fork:` on that agent, and a prompt line - §6.4 |
2057
2085
  | Forks when the steps actually depend | Prompt line: branches cannot see each other |
2058
2086
  | Forks a sequence, or loses a branch's working | `agents/fork-instructions.md` is missing or empty - §6.4 |
2087
+ | Edits files blindly or breaks patches | `agents/files-instructions.md` is missing or empty - §3.6 |
2059
2088
  | Slow and expensive on trivial cases | Demote that agent's model tier / reasoning effort |
2060
2089
  | Fails only on genuinely hard cases | Promote that agent's tier, or split the hard path out |
2061
2090
  | Shows no reasoning while it works | Turn summaries on for that model - §7.6 |