@zenera/cli 1.1.20 → 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.
- package/README.md +3 -3
- package/dist/banner.d.ts +41 -20
- package/dist/banner.js +32 -5
- package/dist/commands/check.js +1 -0
- package/dist/commands/init.js +1 -0
- package/dist/commands/inspect.js +1 -0
- package/dist/commands/key.js +1 -0
- package/dist/commands/list.js +1 -0
- package/dist/commands/memory.js +1 -0
- package/dist/commands/models.js +1 -0
- package/dist/commands/open.js +1 -0
- package/dist/commands/run.js +3 -0
- package/dist/commands/sandbox.js +1 -0
- package/dist/commands/version.js +1 -0
- package/dist/lib.d.ts +3 -2
- package/dist/lib.js +3 -2
- package/dist/meta.d.ts +1 -1
- package/dist/meta.js +5 -28
- package/dist/resolve.js +19 -12
- package/dist/sandbox.js +1 -0
- package/dist/scaffold.js +3 -1
- package/dist/term.d.ts +2 -0
- package/dist/term.js +2 -0
- package/dist/tui/markdown.d.ts +16 -0
- package/dist/tui/markdown.js +259 -0
- package/dist/tui/theme.js +1 -1
- package/dist/tui/wrap.d.ts +1 -1
- package/dist/validate.js +82 -3
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +62 -33
- package/templates/editor/.github/prompts/spec-sync-project.prompt.md +31 -1
- package/templates/editor/.github/skills/zen-cli/SKILL.md +15 -15
- package/templates/editor/.github/skills/zen-cli/references/projects.md +1 -0
- package/templates/editor/.github/skills/zen-cli/references/rag.md +37 -3
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +30 -2
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +232 -0
- package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +554 -0
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +56 -23
- package/templates/editor/.github/skills/zen-rag-schema/SKILL.md +55 -28
- package/templates/editor/.github/skills/zen-review/SKILL.md +17 -1
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +257 -0
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +15 -3
- package/templates/editor/.vscode/agents.schema.json +8 -3
- package/templates/project/SPECIFICATION.md.tmpl +2 -1
- package/templates/project/agents/files-instructions.md +86 -0
- package/templates/project/agents/fork-instructions.md +17 -0
- package/templates/project/agents/memory-instructions.md +37 -0
- package/templates/project/agents.yaml.tmpl +3 -3
- package/templates/project/gitignore +12 -1
- 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
|
|
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);
|
package/dist/tui/wrap.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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.
|
|
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,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
802
|
-
agent must not have - not a hand-picked list of names. The tools
|
|
803
|
-
designed to be used together, and a list assembled one name at
|
|
804
|
-
the companion the model reaches for next: `write_file`
|
|
805
|
-
turns every one-line edit into a whole-file rewrite, and
|
|
806
|
-
`list_dir` and `find_files` leaves it guessing at paths. A
|
|
807
|
-
runtime - a tool added to it later reaches the agent,
|
|
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
|
|
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
|
|
845
|
-
|
|
|
846
|
-
| `read_file`
|
|
847
|
-
| `
|
|
848
|
-
| `workspace:*`
|
|
849
|
-
| `'*'`
|
|
850
|
-
| `-<any>`
|
|
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: [
|
|
876
|
+
tools: [files:*]
|
|
856
877
|
|
|
857
878
|
- name: reviewer
|
|
858
879
|
# Everything except the four that can change something.
|
|
859
|
-
tools: [
|
|
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: [
|
|
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: [
|
|
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:*,
|
|
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
|
|
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 |
|