@zenera/cli 1.1.11 → 1.1.14
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 +20 -0
- package/dist/banner.js +69 -47
- package/dist/commands/index.js +3 -0
- package/dist/commands/init.js +8 -4
- package/dist/commands/inspect.js +23 -5
- package/dist/commands/memory.d.ts +3 -0
- package/dist/commands/memory.js +375 -0
- package/dist/engine.js +15 -9
- package/dist/main.js +9 -0
- package/dist/podman.js +43 -9
- package/dist/projects.js +10 -2
- package/dist/scaffold.js +22 -1
- package/dist/tui/app.js +19 -11
- package/dist/tui/theme.d.ts +7 -0
- package/dist/tui/theme.js +18 -11
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +151 -43
- package/templates/editor/.github/prompts/review-project.prompt.md +5 -0
- package/templates/editor/.github/prompts/sync-with-spec.prompt.md +7 -4
- package/templates/editor/.github/skills/zen-cli/SKILL.md +3 -0
- package/templates/editor/.github/skills/zen-cli/references/memory.md +113 -0
- package/templates/editor/.github/skills/zen-cli/references/rag.md +2 -2
- package/templates/editor/.github/skills/zen-cli/references/run.md +3 -1
- package/templates/editor/.github/skills/zen-memory/SKILL.md +555 -0
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +16 -16
- package/templates/editor/.github/skills/zen-rag-schema/SKILL.md +2 -2
- package/templates/editor/.vscode/settings.json +4 -2
- package/templates/parts/exa.prompt.md.tmpl +4 -0
- package/templates/parts/exa.spec.md.tmpl +2 -0
- package/templates/project/INSTRUCTIONS.md +8 -0
- package/templates/project/SPECIFICATION.md.tmpl +47 -0
- package/templates/project/agents/prompts/{default.md → default.md.tmpl} +4 -0
- package/templates/project/agents.yaml.tmpl +4 -0
- package/templates/project/gitignore +7 -1
- package/templates/project/scripts/_setup.sh +101 -0
- package/templates/project/SPECIFICATION.md +0 -6
package/dist/tui/app.js
CHANGED
|
@@ -35,9 +35,9 @@ function Row({ line }) {
|
|
|
35
35
|
const style = useTheme().line[line.kind];
|
|
36
36
|
return (_jsxs(Box, { flexDirection: "row", marginTop: line.kind === 'you' ? 1 : 0, children: [_jsxs(Text, { color: style.color, dimColor: style.dim, children: [MARK[line.kind], ' '] }), _jsxs(Box, { flexDirection: "column", children: [line.kind === 'agent' ? (_jsx(Answer, { text: line.text })) : (
|
|
37
37
|
// The lead is what the eye scans for down the left edge, so
|
|
38
|
-
// it
|
|
38
|
+
// it carries the weight the row's text does not. Nested
|
|
39
39
|
// rather than one string: Ink dims a whole Text or none of it.
|
|
40
|
-
_jsxs(Text, { children: [line.lead ? _jsx(Text, { color: style.color, children: `${line.lead} ` }) : null, _jsx(Text, { color: style.color, dimColor: style.dim, bold: line.kind === 'you', children: line.text })] })), line.detail ? _jsx(Text, { dimColor: true, children: line.detail }) : null] })] }));
|
|
40
|
+
_jsxs(Text, { children: [line.lead ? _jsx(Text, { color: style.color, bold: true, children: `${line.lead} ` }) : null, _jsx(Text, { color: style.color, dimColor: style.dim, bold: line.kind === 'you', children: line.text })] })), line.detail ? _jsx(Text, { dimColor: true, children: line.detail }) : null] })] }));
|
|
41
41
|
}
|
|
42
42
|
function Answer({ text }) {
|
|
43
43
|
const { stdout } = useStdout();
|
|
@@ -511,11 +511,13 @@ function branchBoxesOf(tools, branches, now, width, lane, share) {
|
|
|
511
511
|
}),
|
|
512
512
|
// Between the calls it made and the one it is making: the reasoning
|
|
513
513
|
// is what got it from one to the other.
|
|
514
|
-
...(gist
|
|
514
|
+
...(gist
|
|
515
|
+
? [{ key: `g:${b.name}`, label: `… ${gist}`, detail: '', thinking: true }]
|
|
516
|
+
: []),
|
|
515
517
|
...live,
|
|
516
518
|
];
|
|
517
519
|
if (!rows.length) {
|
|
518
|
-
rows.push({ key: `w:${b.name}`, label: 'thinking…', detail: '',
|
|
520
|
+
rows.push({ key: `w:${b.name}`, label: 'thinking…', detail: '', thinking: true });
|
|
519
521
|
}
|
|
520
522
|
boxes.push({
|
|
521
523
|
name: b.name,
|
|
@@ -572,18 +574,19 @@ function fitActivity(boxes, trunk, allowance) {
|
|
|
572
574
|
};
|
|
573
575
|
}
|
|
574
576
|
function Branches({ boxes, spin, columns, }) {
|
|
577
|
+
const theme = useTheme();
|
|
575
578
|
return (_jsx(Box, { flexDirection: "column", children: boxes.map((b) => {
|
|
576
579
|
// Ink truncates rather than wraps, so an over-wide rule clips
|
|
577
580
|
// instead of costing the box a row it was not given.
|
|
578
581
|
const used = 3 + b.title.length + 2 + 2 + b.stats.length + 1;
|
|
579
|
-
return (_jsxs(Box, { flexDirection: "column", height: BOX_CHROME + b.rows.length, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: '╭─ ' }), _jsx(Text, { color: b.color, children: b.title }), _jsx(Text, { color: b.color, children: ` ${spin}` }), _jsx(Text, { dimColor: true, children: ` ${b.stats} ` }), _jsx(Text, { dimColor: true, children: '─'.repeat(Math.max(0, columns - used)) })] }), b.rows.map((r) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: b.color, children: '│ ' }), _jsx(Text, { dimColor:
|
|
582
|
+
return (_jsxs(Box, { flexDirection: "column", height: BOX_CHROME + b.rows.length, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: '╭─ ' }), _jsx(Text, { color: b.color, children: b.title }), _jsx(Text, { color: b.color, children: ` ${spin}` }), _jsx(Text, { dimColor: true, children: ` ${b.stats} ` }), _jsx(Text, { dimColor: true, children: '─'.repeat(Math.max(0, columns - used)) })] }), b.rows.map((r) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: b.color, children: '│ ' }), _jsx(Text, { color: r.thinking ? theme.thinking.color : undefined, dimColor: r.thinking ? theme.thinking.dim : undefined, children: r.label }), _jsx(Text, { dimColor: true, children: r.detail })] }, r.key))), _jsx(Text, { dimColor: true, children: '╰─' })] }, b.name));
|
|
580
583
|
}) }));
|
|
581
584
|
}
|
|
582
585
|
function Activity({ rows, hidden, }) {
|
|
583
586
|
if (!rows.length && !hidden) {
|
|
584
587
|
return null;
|
|
585
588
|
}
|
|
586
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows.length + (hidden ? 1 : 0), overflow: "hidden", children: [rows.map((r) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: r.color,
|
|
589
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows.length + (hidden ? 1 : 0), overflow: "hidden", children: [rows.map((r) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: r.color, children: r.label }), _jsx(Text, { dimColor: true, children: r.detail })] }, r.key))), hidden ? (_jsx(Text, { dimColor: true, children: ` + ${hidden} more ${hidden === 1 ? 'branch' : 'branches'}` })) : null] }));
|
|
587
590
|
}
|
|
588
591
|
/**
|
|
589
592
|
* A stable colour per branch, handed out in order of first sight. Held in a ref
|
|
@@ -635,11 +638,13 @@ function Thinking({ text, columns, rows, spin, live, }) {
|
|
|
635
638
|
// Ink truncates rather than wraps, so an over-wide rule clips instead of
|
|
636
639
|
// costing the block a row it was not given.
|
|
637
640
|
const dashes = Math.max(0, columns - TITLE.length - (live ? 6 : 4));
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
//
|
|
641
|
+
const { color, dim } = theme.thinking;
|
|
642
|
+
return (_jsxs(Box, { flexDirection: "column", height: shown.length + THINKING_CHROME, overflow: "hidden", children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: '╭─ ' }), _jsx(Text, { color: color, children: TITLE }), live ? _jsx(Text, { color: color, children: ` ${spin}` }) : null, _jsx(Text, { dimColor: true, children: ` ${'─'.repeat(dashes)}` })] }), shown.map((row, i) => {
|
|
643
|
+
// Not italic, and not dim: dim italic on a dark terminal is the
|
|
644
|
+
// least legible thing available, and this is meant to be read.
|
|
645
|
+
// Its own hue is what separates it from the tool rows instead.
|
|
641
646
|
const head = HEADING.exec(row);
|
|
642
|
-
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, {
|
|
647
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: color, children: '│ ' }), _jsx(Text, { color: color, dimColor: dim, bold: head !== null, children: head ? head[1] : row })] }, i));
|
|
643
648
|
}), _jsx(Text, { dimColor: true, children: '╰─' })] }));
|
|
644
649
|
}
|
|
645
650
|
function Live({ text, columns, rows }) {
|
|
@@ -679,7 +684,10 @@ function Footer({ agent, busy, spin, running, forked, step, model, stats, inflig
|
|
|
679
684
|
: 'thinking';
|
|
680
685
|
// A clip has already ended it with one.
|
|
681
686
|
const status = what.endsWith('…') ? what : `${what}…`;
|
|
682
|
-
|
|
687
|
+
// Nothing of its own in flight means the word above is about reasoning, so
|
|
688
|
+
// it takes the same hue the reasoning block does.
|
|
689
|
+
const musing = !running.length && !forked;
|
|
690
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: agent }), busy ? (_jsxs(Text, { color: musing ? theme.thinking.color : theme.warn, children: [' ', spin, " ", status] })) : null, busy ? _jsx(Text, { dimColor: true, children: ` ${aside}` }) : null] }), inflight ? (_jsxs(Text, { dimColor: true, children: ['this turn'.padEnd(LABEL), tokens(inflight.usage), ` · ${secs(inflight.durationMs)}`] })) : (_jsxs(_Fragment, { children: [stats.turn ? (_jsxs(Text, { dimColor: true, children: ['last turn'.padEnd(LABEL), tokens(stats.turn), stats.durationMs === undefined
|
|
683
691
|
? ''
|
|
684
692
|
: ` · ${durationOf(stats.durationMs) ?? ''}`] })) : null, _jsxs(Text, { dimColor: true, children: ['session'.padEnd(LABEL), tokens(stats.session), stats.calls
|
|
685
693
|
? ` · ${stats.calls} ${stats.calls === 1 ? 'call' : 'calls'}`
|
package/dist/tui/theme.d.ts
CHANGED
|
@@ -13,6 +13,13 @@ export interface Theme {
|
|
|
13
13
|
readonly accent: string;
|
|
14
14
|
/** Read-only badge, busy label. */
|
|
15
15
|
readonly warn: string;
|
|
16
|
+
/**
|
|
17
|
+
* Reasoning. It is not a tool call and it is not the answer, and drawn in
|
|
18
|
+
* the same dim foreground as the rows around it there was nothing to say
|
|
19
|
+
* which — a wall of one weight reads as one thing. Its own hue is what
|
|
20
|
+
* separates the model's thinking from the work it went on to do.
|
|
21
|
+
*/
|
|
22
|
+
readonly thinking: LineStyle;
|
|
16
23
|
/**
|
|
17
24
|
* Branch colours, cycled in order of first sight. A fan-out is the one
|
|
18
25
|
* place where colour carries information rather than decoration: eight
|
package/dist/tui/theme.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// A terminal already has a colour scheme, and it is not ours to replace. The
|
|
5
5
|
// rule here is to name as few colours as possible and to name them by role:
|
|
6
6
|
// the answer is drawn in the terminal's *own* foreground (no colour at all),
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// and only the few things that must stand out — the person's own turn, the
|
|
8
|
+
// agent name, what it is reasoning about, a warning, an error — take a colour.
|
|
9
9
|
//
|
|
10
10
|
// That alone fixes most of it. `white` was the bug: it is legible on exactly
|
|
11
11
|
// one kind of background, and half the world runs the other kind. `gray` was
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
// unreadable on dark and invisible on paper. What is left is the handful of
|
|
14
14
|
// accents that ANSI *does* let a light theme get wrong, so those swap.
|
|
15
15
|
//
|
|
16
|
+
// Dim is spent on *chrome* — box rules, clocks, the footer's numbers — and no
|
|
17
|
+
// longer on content. Tool rows are most of what is on screen, and dimming them
|
|
18
|
+
// put the bulk of the transcript one step from unreadable: the whole thing read
|
|
19
|
+
// as too dark. They are content, so they get the foreground like content does.
|
|
20
|
+
//
|
|
16
21
|
// Note what is *not* here: no hex, no 256-colour ramps, no attempt at a brand.
|
|
17
22
|
// A palette that ignores the user's scheme is worse on both schemes than one
|
|
18
23
|
// that mostly defers to it.
|
|
@@ -22,16 +27,17 @@ const DARK = {
|
|
|
22
27
|
line: {
|
|
23
28
|
you: { color: 'cyan' },
|
|
24
29
|
agent: {},
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
tool: {
|
|
29
|
-
note: { color: 'cyan'
|
|
30
|
+
// Never dim, and never `gray`: bright black is already the faintest
|
|
31
|
+
// colour a terminal has, and either one puts the bulk of the
|
|
32
|
+
// transcript a step from unreadable.
|
|
33
|
+
tool: {},
|
|
34
|
+
note: { color: 'cyan' },
|
|
30
35
|
error: { color: 'red' },
|
|
31
36
|
},
|
|
32
37
|
accent: 'cyan',
|
|
33
38
|
warn: 'yellow',
|
|
34
|
-
|
|
39
|
+
thinking: { color: 'magenta' },
|
|
40
|
+
lanes: ['cyan', 'green', 'yellow', 'blue', 'red', 'magenta'],
|
|
35
41
|
};
|
|
36
42
|
// On a light background `cyan` and `yellow` are barely darker than the paper.
|
|
37
43
|
// Dimmed default foreground and `blue`/`magenta` are the same information,
|
|
@@ -41,14 +47,15 @@ const LIGHT = {
|
|
|
41
47
|
line: {
|
|
42
48
|
you: { color: 'blue' },
|
|
43
49
|
agent: {},
|
|
44
|
-
tool: {
|
|
45
|
-
note: { color: 'blue'
|
|
50
|
+
tool: {},
|
|
51
|
+
note: { color: 'blue' },
|
|
46
52
|
error: { color: 'red' },
|
|
47
53
|
},
|
|
48
54
|
accent: 'blue',
|
|
49
55
|
warn: 'magenta',
|
|
56
|
+
thinking: { color: 'magenta' },
|
|
50
57
|
// No cyan or yellow: on paper they are barely darker than the paper.
|
|
51
|
-
lanes: ['
|
|
58
|
+
lanes: ['blue', 'green', 'red', 'magenta'],
|
|
52
59
|
};
|
|
53
60
|
export const THEMES = { dark: DARK, light: LIGHT };
|
|
54
61
|
export function parseChoice(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
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.14",
|
|
55
55
|
"@anthropic-ai/sdk": "^0.120.0",
|
|
56
56
|
"@google/genai": "^2.18.0",
|
|
57
57
|
"@openrouter/sdk": "^1.2.80",
|
|
@@ -141,8 +141,9 @@ my-project/
|
|
|
141
141
|
│ ├── SKILL.md
|
|
142
142
|
│ └── scripts/calculate.py run at /skills/refund_policy/scripts — §3.4.1
|
|
143
143
|
├── assets/ reference material, read-only at /assets
|
|
144
|
+
├── memory/ the graph agents remember into — §5.3
|
|
144
145
|
├── sandbox/Dockerfile the image commands run in — §3.7
|
|
145
|
-
└── sessions/ run state,
|
|
146
|
+
└── sessions/ run state, whatever the agent wrote
|
|
146
147
|
```
|
|
147
148
|
|
|
148
149
|
Only `agents.yaml` is required, and only `agents:` is required inside it.
|
|
@@ -224,6 +225,81 @@ agents do, for the same reason — it is a prefix somebody pays for:
|
|
|
224
225
|
hand, so **edits inside `.github/` do not survive**. Project-specific conventions
|
|
225
226
|
belong in `INSTRUCTIONS.md` and the agent prompts, which are never overwritten.
|
|
226
227
|
|
|
228
|
+
### 2.5 What a running agent can actually see
|
|
229
|
+
|
|
230
|
+
The tree in §2.1 is the **author's** view. A running agent sees almost none of
|
|
231
|
+
it. Its entire world is the tools it was granted (§3.5) and four paths:
|
|
232
|
+
|
|
233
|
+
| Path | Is | Access |
|
|
234
|
+
| ------------ | ----------------------------------------- | ------------------------------------------------------ |
|
|
235
|
+
| `/workspace` | the session's workspace — the work itself | read/write; `zen run --read-only` withholds the writes |
|
|
236
|
+
| `/assets` | `assets/`, reference material — §3.11 | read-only |
|
|
237
|
+
| `/skills` | the whole skill catalog — §3.4.1 | read-only |
|
|
238
|
+
| `/memory` | files carried by remembered nodes — §5.3 | read-only, and only when `memory:` is configured |
|
|
239
|
+
|
|
240
|
+
Commands run inside the container `sandbox:` describes (§3.7): the same
|
|
241
|
+
`/workspace`, and whatever the image ships.
|
|
242
|
+
|
|
243
|
+
Everything else in the project directory is **invisible at run time** —
|
|
244
|
+
`agents.yaml`, `INSTRUCTIONS.md`, `agents/prompts/`, `agents/skills/` under that
|
|
245
|
+
name, `sandbox/Dockerfile`, `.env`, `.github/`, `sessions/`, and the project
|
|
246
|
+
root itself. `zen` reads them to build the run; they are never a path an agent
|
|
247
|
+
can open.
|
|
248
|
+
|
|
249
|
+
Two consequences:
|
|
250
|
+
|
|
251
|
+
- **The workspace is not the project.** It defaults to the session's own empty
|
|
252
|
+
folder, and is usually some other repository entirely — `--workspace`, or the
|
|
253
|
+
directory the user was standing in. A prompt saying "read `agents.yaml`" or
|
|
254
|
+
"the rules are in `agents/skills/`" names a file that is not there.
|
|
255
|
+
- **Write the mount path, never the repository path.**
|
|
256
|
+
`/skills/refund_policy/rates.csv`, not
|
|
257
|
+
`agents/skills/refund_policy/rates.csv`; `/assets/handbook.md`, not
|
|
258
|
+
`assets/handbook.md`. A relative path resolves against `/workspace`, which is
|
|
259
|
+
the one place the file is not.
|
|
260
|
+
|
|
261
|
+
An agent that must know something about its own configuration is **told it in
|
|
262
|
+
prose**, in the prompt — never sent to read the file that configured it.
|
|
263
|
+
|
|
264
|
+
#### Checklist — no path an agent cannot open
|
|
265
|
+
|
|
266
|
+
Run this over `INSTRUCTIONS.md`, every `agents/prompts/*.md` and every
|
|
267
|
+
`agents/skills/*/SKILL.md` after editing any of them. Every path-shaped token in
|
|
268
|
+
those files is read by a model as an instruction to open something:
|
|
269
|
+
|
|
270
|
+
- [ ] Every absolute path begins `/workspace`, `/assets`, `/skills` or
|
|
271
|
+
`/memory`. Nothing else exists.
|
|
272
|
+
- [ ] No project file is named: `agents.yaml`, `INSTRUCTIONS.md`,
|
|
273
|
+
`agents/prompts/…`, `agents/skills/…`, `sandbox/Dockerfile`, `.env`,
|
|
274
|
+
`.github/…`, `sessions/…`, `SPECIFICATION.md`, `README.md`, `docs/…`.
|
|
275
|
+
- [ ] Nothing escapes a mount: no `../`, no `~`, no host path (`/Users/…`,
|
|
276
|
+
`/home/<someone>`), no `/etc`, `/var` or `/tmp` expected to hold anything
|
|
277
|
+
the project put there.
|
|
278
|
+
- [ ] Each `/assets/…` and `/skills/<name>/…` path names a file that is actually
|
|
279
|
+
committed there — `zen check` lists what every skill folder ships (§3.4.1).
|
|
280
|
+
- [ ] A skill referring to its own sibling file writes the full
|
|
281
|
+
`/skills/<name>/` prefix, not `./rates.csv`: the body is rendered into a
|
|
282
|
+
prompt, not executed from its folder.
|
|
283
|
+
- [ ] `/memory` is named only in prompts for agents that have `memory:` (§5.3),
|
|
284
|
+
and `/assets` only where an `assets/` directory exists.
|
|
285
|
+
- [ ] The agent holding the prompt has the tool the path implies — `read_file`
|
|
286
|
+
to open one, `sandbox:*` to run one (§3.4.1).
|
|
287
|
+
- [ ] Every instruction that opens a file states what to do when it is not there
|
|
288
|
+
(§4.2, rule 6). Workspaces differ between runs; absence is normal.
|
|
289
|
+
- [ ] Relative paths appear only where `/workspace`-relative is what was meant.
|
|
290
|
+
|
|
291
|
+
Finding the candidates, from the project root — every path-shaped token in the
|
|
292
|
+
three trees, minus the ones under a mount:
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
grep -Eohr '[~.]?/?[A-Za-z0-9_.-]*/[A-Za-z0-9_.*-]+' \
|
|
296
|
+
INSTRUCTIONS.md agents/prompts agents/skills \
|
|
297
|
+
| sort -u | grep -Ev '^/(workspace|assets|skills|memory)(/|$)'
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Everything it prints is either a false positive (a url, a `path/to` in a
|
|
301
|
+
sentence, `and/or`) or a path the agent cannot open. There is no third case.
|
|
302
|
+
|
|
227
303
|
---
|
|
228
304
|
|
|
229
305
|
## 3. File formats
|
|
@@ -1031,22 +1107,38 @@ agents:
|
|
|
1031
1107
|
|
|
1032
1108
|
### 5.3 Memory
|
|
1033
1109
|
|
|
1034
|
-
Memory is the read-write twin of skills: written by the run, not authored.
|
|
1035
|
-
**
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
are injected before each call, which is what makes memory work without the
|
|
1039
|
-
model remembering to look, and an agent with write access also gets tools to
|
|
1040
|
-
search, add, update and delete entries.
|
|
1110
|
+
Memory is the read-write twin of skills: written by the run, not authored. It is
|
|
1111
|
+
a **graph** — a node is one thing worth keeping, an edge says how one led to
|
|
1112
|
+
another — so recall returns the connected piece rather than a ranked list, and a
|
|
1113
|
+
remembered script arrives with the request that asked for it.
|
|
1041
1114
|
|
|
1042
|
-
It is
|
|
1043
|
-
|
|
1044
|
-
|
|
1115
|
+
It is configured in `agents.yaml`: a top-level `memory:` block says where the
|
|
1116
|
+
graph lives and what vectorises it, and `agents[].memory` says what each agent
|
|
1117
|
+
may do with it. It lives with the **project**, not the session — a memory that
|
|
1118
|
+
died with the session would be a cache. Recall is automatic on turns that follow
|
|
1119
|
+
new user input, which is what makes memory work without the model remembering to
|
|
1120
|
+
look.
|
|
1121
|
+
|
|
1122
|
+
```yaml
|
|
1123
|
+
memory:
|
|
1124
|
+
embedding: main # without one, recall falls back to term overlap
|
|
1125
|
+
agents:
|
|
1126
|
+
- name: auditor
|
|
1127
|
+
memory: true # read, write, and recall before each new question
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
Every node carries an **audience**, and an agent's `sees` is the set it reads:
|
|
1131
|
+
one graph, masked per agent, never one graph each. The four `memory_*` tools are
|
|
1132
|
+
derived from `access` and are never listed in `tools:`.
|
|
1045
1133
|
|
|
1046
1134
|
Do not treat memory as a database. It is for things learned that should
|
|
1047
1135
|
persist. Anything authoritative — a rate, a policy clause, a procedure — belongs
|
|
1048
1136
|
in a skill, where it is versioned and reviewable.
|
|
1049
1137
|
|
|
1138
|
+
The full treatment — kinds and relations, how ranking and stitching spend their
|
|
1139
|
+
budget, remembered files under `/memory`, audience design, and how to put memory
|
|
1140
|
+
in front of a `zen rag` index — is in the `zen-memory` skill.
|
|
1141
|
+
|
|
1050
1142
|
### 5.4 Cache discipline
|
|
1051
1143
|
|
|
1052
1144
|
Rules that follow directly from "stable prefix = cache hit":
|
|
@@ -1601,9 +1693,9 @@ zen models pick --embedding # or --chat; the ref goes to stdout, alone
|
|
|
1601
1693
|
Then re-run `zen check`.
|
|
1602
1694
|
|
|
1603
1695
|
CLI (`zen --help` for the authoritative list): `zen init`, `zen run`, `zen check`,
|
|
1604
|
-
`zen inspect`, `zen key`, `zen models`, `zen list`. **stdout is the
|
|
1605
|
-
is the narration**; every command takes `--json`. Exit codes: `0`
|
|
1606
|
-
`2` usage, `3` invalid project, `4` no usable credential.
|
|
1696
|
+
`zen inspect`, `zen memory`, `zen key`, `zen models`, `zen list`. **stdout is the
|
|
1697
|
+
answer, stderr is the narration**; every command takes `--json`. Exit codes: `0`
|
|
1698
|
+
ok, `1` failed, `2` usage, `3` invalid project, `4` no usable credential.
|
|
1607
1699
|
|
|
1608
1700
|
---
|
|
1609
1701
|
|
|
@@ -1630,6 +1722,16 @@ Before finishing any change here:
|
|
|
1630
1722
|
- [ ] No facts, rates or figures embedded in a prompt
|
|
1631
1723
|
- [ ] No hedging, no meta-talk about the runtime
|
|
1632
1724
|
|
|
1725
|
+
**Paths (§2.5 — run its checklist over the three trees)**
|
|
1726
|
+
|
|
1727
|
+
- [ ] Every absolute path in `INSTRUCTIONS.md`, `agents/prompts/*.md` and
|
|
1728
|
+
`agents/skills/*/SKILL.md` is under `/workspace`, `/assets`, `/skills` or
|
|
1729
|
+
`/memory`
|
|
1730
|
+
- [ ] No prompt or skill names a project file — `agents.yaml`, a prompt path, a
|
|
1731
|
+
skill's repository path, `sandbox/Dockerfile`, `.env`, `sessions/`
|
|
1732
|
+
- [ ] No `../`, `~`, or host path; nothing assumes the workspace is the project
|
|
1733
|
+
- [ ] Every `/assets/…` and `/skills/<name>/…` path names a committed file
|
|
1734
|
+
|
|
1633
1735
|
**Skills**
|
|
1634
1736
|
|
|
1635
1737
|
- [ ] Every skill is `<name>/SKILL.md` — no bare `<name>.md` in the catalog
|
|
@@ -1674,35 +1776,37 @@ Before finishing any change here:
|
|
|
1674
1776
|
|
|
1675
1777
|
## 10. Where to change what
|
|
1676
1778
|
|
|
1677
|
-
| Symptom
|
|
1678
|
-
|
|
|
1679
|
-
| Wrong tone, wrong format, wrong length
|
|
1680
|
-
| Says something forbidden
|
|
1681
|
-
| Ignores a rule that only applies sometimes
|
|
1682
|
-
| Never loads the skill it should
|
|
1683
|
-
| Loads too much, answers slowly
|
|
1684
|
-
| Invents a number
|
|
1685
|
-
| A skill in the catalog is never offered
|
|
1686
|
-
| Cannot find a file its own skill names
|
|
1687
|
-
|
|
|
1688
|
-
|
|
|
1689
|
-
|
|
|
1690
|
-
|
|
|
1691
|
-
|
|
|
1692
|
-
|
|
|
1693
|
-
|
|
|
1694
|
-
|
|
|
1695
|
-
|
|
|
1696
|
-
|
|
|
1697
|
-
|
|
|
1698
|
-
|
|
|
1699
|
-
|
|
|
1700
|
-
|
|
|
1701
|
-
|
|
|
1702
|
-
|
|
|
1703
|
-
|
|
|
1704
|
-
|
|
|
1705
|
-
|
|
|
1779
|
+
| Symptom | Change this |
|
|
1780
|
+
| --------------------------------------------- | ------------------------------------------------------------------- |
|
|
1781
|
+
| Wrong tone, wrong format, wrong length | `INSTRUCTIONS.md` (all agents) or the agent prompt |
|
|
1782
|
+
| Says something forbidden | `INSTRUCTIONS.md` prohibition, stated specifically |
|
|
1783
|
+
| Ignores a rule that only applies sometimes | Move the rule into a skill with a sharp description |
|
|
1784
|
+
| Never loads the skill it should | The skill's `description`; or `preload` it |
|
|
1785
|
+
| Loads too much, answers slowly | `allow:`, `maxIndexEntries:`, or `discovery: search` |
|
|
1786
|
+
| Invents a number | A skill holding the figure, or a script that computes it — §3.4.1 |
|
|
1787
|
+
| A skill in the catalog is never offered | It is a bare `<name>.md`; move it to `<name>/SKILL.md` — §3.4 |
|
|
1788
|
+
| Cannot find a file its own skill names | Absolute `/skills/<name>/…` path, and `sandbox:*` — §3.4.1 |
|
|
1789
|
+
| Tries to open `agents.yaml` or its own prompt | The prompt names a file no agent can see — §2.5 |
|
|
1790
|
+
| Says a file is missing that is in the project | The workspace is not the project directory — §2.5 |
|
|
1791
|
+
| Rewrites a whole file to change one line | A prompt line preferring `apply_patch` — §3.6 |
|
|
1792
|
+
| Edits files it should only be reading | Subtract the mutating tools, or `zen run --read-only` |
|
|
1793
|
+
| Cannot run the build or the tests | Grant `sandbox:*`; add the toolchain to `sandbox/Dockerfile` |
|
|
1794
|
+
| Installs the same packages on every run | Put them in `sandbox/Dockerfile`, or `sandbox.persist: true` — §3.7 |
|
|
1795
|
+
| Answers from stale knowledge of the world | Grant `web_search` + `web_read`, and say when — §3.8 |
|
|
1796
|
+
| Cites a page it only saw the excerpt of | A prompt line: `web_read` before quoting — §3.8 |
|
|
1797
|
+
| Every web call refuses | No Exa key: `zen key add exa` — `zen check` warns — §3.8 |
|
|
1798
|
+
| A model or embedder refuses every call | `zen models test <ref>` — if `blocked`, `zen models pick` — §8 |
|
|
1799
|
+
| Answers instead of routing | Router prompt prohibition; check `handoffs:` |
|
|
1800
|
+
| Routes to the wrong specialist | The target agents' `description:` fields |
|
|
1801
|
+
| Gets stuck in the agent it routed to | The target needs a handoff back — §6.3 |
|
|
1802
|
+
| Loses a detail after a handoff | Say it in the handoff; check the collapse policy |
|
|
1803
|
+
| Works through N independent items serially | `fork:` on that agent, and a prompt line — §6.4 |
|
|
1804
|
+
| Forks when the steps actually depend | Prompt line: branches cannot see each other |
|
|
1805
|
+
| Slow and expensive on trivial cases | Demote that agent's model tier / reasoning effort |
|
|
1806
|
+
| Fails only on genuinely hard cases | Promote that agent's tier, or split the hard path out |
|
|
1807
|
+
| Shows no reasoning while it works | Turn summaries on for that model — §7.6 |
|
|
1808
|
+
| Forgets across conversations | Continue the session rather than starting a new one |
|
|
1809
|
+
| Breaks at load with a named path | Read the message — it names the exact key |
|
|
1706
1810
|
|
|
1707
1811
|
---
|
|
1708
1812
|
|
|
@@ -1715,6 +1819,10 @@ Before finishing any change here:
|
|
|
1715
1819
|
versioned, cannot be shared, and is paid for on every call.
|
|
1716
1820
|
- **The bare skill file.** `agents/skills/<name>.md` instead of a folder. It can
|
|
1717
1821
|
never grow the table or script the next revision of the rule will want — §3.4.
|
|
1822
|
+
- **Pointing an agent at the project.** A prompt or skill naming `agents.yaml`,
|
|
1823
|
+
`INSTRUCTIONS.md`, `agents/skills/<name>/…` or `sandbox/Dockerfile`. None of
|
|
1824
|
+
them are on any path the agent can open, so the instruction fails on the turn
|
|
1825
|
+
that follows it — §2.5.
|
|
1718
1826
|
- **Arithmetic in prose.** A skill that walks the model through a calculation it
|
|
1719
1827
|
will get wrong, in a folder that could have held the script — §3.4.1.
|
|
1720
1828
|
- **Politeness padding.** "Please try your best to be helpful." Costs tokens,
|
|
@@ -17,6 +17,11 @@ Work the checklist in §9 of the copilot instructions. In particular:
|
|
|
17
17
|
`INSTRUCTIONS.md` that is true of only one agent.
|
|
18
18
|
- **Facts.** Any number, threshold, fee or date living in a prompt is in the
|
|
19
19
|
wrong file. Name it and say which skill it belongs in.
|
|
20
|
+
- **Paths.** Work the checklist in §2.5 over `INSTRUCTIONS.md`,
|
|
21
|
+
`agents/prompts/*.md` and `agents/skills/*/SKILL.md`. Every absolute path must
|
|
22
|
+
be under `/workspace`, `/assets`, `/skills` or `/memory`; nothing may name a
|
|
23
|
+
project file the agent cannot open, and nothing may assume the workspace is
|
|
24
|
+
this directory.
|
|
20
25
|
- **Skill descriptions.** Each one a condition, not a title; no two overlapping.
|
|
21
26
|
- **Tools.** Every grant justified by the agent's job; nothing mutating held by
|
|
22
27
|
an agent that only reads; every granted tool mentioned by the prompt.
|
|
@@ -98,16 +98,19 @@ not a paragraph of instructions for a human.
|
|
|
98
98
|
- `scripts/_setup.sh` is the **only** entry point — the leading underscore is
|
|
99
99
|
what separates the runner from the steps it runs. It runs the steps in the
|
|
100
100
|
order they depend on each other and is the single command that initialises the
|
|
101
|
-
project.
|
|
102
|
-
|
|
101
|
+
project. `zen init` writes it; adding a step is adding `scripts/<name>.sh` and
|
|
102
|
+
its name to the `STEPS` list at the top of it. Consolidate any setup script
|
|
103
|
+
that lives elsewhere into this shape, including one left at the project root.
|
|
103
104
|
- Every script starts `set -eu`, works from any working directory
|
|
104
105
|
(`cd "$(dirname "$0")/.."` first), needs no arguments, and exits non-zero on
|
|
105
106
|
failure so `scripts/_setup.sh` stops rather than continuing on a broken step.
|
|
106
107
|
- **Re-entrancy is required.** Running `scripts/_setup.sh` a second time must be
|
|
107
108
|
safe. A step whose output is present and newer than its inputs prints that it
|
|
108
|
-
is up to date and
|
|
109
|
+
is up to date and exits **3**, which is how the runner tells `skipped` from
|
|
110
|
+
`ok`; otherwise it redoes the work idempotently, writing
|
|
109
111
|
to a temporary path and moving it into place so an interrupted run never leaves
|
|
110
|
-
a half-built artefact behind. `scripts/_setup.sh --force` redoes everything
|
|
112
|
+
a half-built artefact behind. `scripts/_setup.sh --force` redoes everything,
|
|
113
|
+
and reaches each step as `$FORCE=1`.
|
|
111
114
|
- **It must be watchable.** `scripts/_setup.sh` tees each step's output to
|
|
112
115
|
`.tmp/logs/setup-<step>.log`, prints a heartbeat line while a long step runs,
|
|
113
116
|
and finishes with one line per step: `ok`, `skipped`, or `failed`.
|
|
@@ -24,6 +24,7 @@ zen key <ls|add|use|check|rm|show|env> [ref] [options]
|
|
|
24
24
|
zen models <providers|ls|search|show|test|pick> [ref] [options]
|
|
25
25
|
zen check [name|dir] [--project <name|dir>] [--no-sandbox] [--no-models] [--strict] [--quiet]
|
|
26
26
|
zen inspect [run] [--session <id>] [--open] [--rebuild] [--serve [port]]
|
|
27
|
+
zen memory [stats|ls|show|export|forget] [args] [options]
|
|
27
28
|
zen sandbox [status|up|pull|clean|disk] [options]
|
|
28
29
|
zen version
|
|
29
30
|
|
|
@@ -48,6 +49,7 @@ guess a flag, and do not read them all.
|
|
|
48
49
|
| Which models exist, whether one works, recovering from a refusal | [models.md](./references/models.md) |
|
|
49
50
|
| The container shell commands run in, images, `persist` | [sandbox.md](./references/sandbox.md) |
|
|
50
51
|
| Run reports, trajectories, what a session directory holds | [inspect.md](./references/inspect.md) |
|
|
52
|
+
| What the agents remember, why something was recalled, forgetting | [memory.md](./references/memory.md) |
|
|
51
53
|
| `zen faker` — a mock API from an OpenAPI/Swagger document | [faker.md](./references/faker.md) |
|
|
52
54
|
| `zen rag` — searching an API description, or a pile of markdown | [rag.md](./references/rag.md) |
|
|
53
55
|
|
|
@@ -59,6 +61,7 @@ zen check validate everything before spending a turn
|
|
|
59
61
|
zen run the TUI
|
|
60
62
|
zen run "what changed?" one answer, this directory as the workspace
|
|
61
63
|
zen inspect --open what the model was actually given
|
|
64
|
+
zen memory export --open the whole memory graph, as a page
|
|
62
65
|
zen key ls --check which credentials still work
|
|
63
66
|
zen models test <ref> whether one model actually answers
|
|
64
67
|
zen models pick --embedding the first embedder that does, on stdout
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Memory — `zen memory`
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
zen memory [stats|ls|show|export|forget] [args] [options]
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Alias: `mem`.
|
|
8
|
+
|
|
9
|
+
The memory graph from outside the agents. It exists to answer the question
|
|
10
|
+
memory always eventually raises: **why did it recall that?**
|
|
11
|
+
|
|
12
|
+
Only projects that enable memory have one, and the commands say so rather than
|
|
13
|
+
inventing an empty graph. Turning it on is the `memory:` block in `agents.yaml`.
|
|
14
|
+
|
|
15
|
+
This document is the command. For how the graph is organised, how to configure
|
|
16
|
+
it, and how to decide what an agent should remember, read the `zen-memory`
|
|
17
|
+
skill.
|
|
18
|
+
|
|
19
|
+
## Subcommands
|
|
20
|
+
|
|
21
|
+
| Command | What it does |
|
|
22
|
+
| ------------------------- | ------------------------------------------- |
|
|
23
|
+
| `zen memory stats` | Size, vocabulary, whether it is embedded |
|
|
24
|
+
| `zen memory ls` | Nodes, newest first. Changes nothing |
|
|
25
|
+
| `zen memory show <id>` | One node in full, with what it links to |
|
|
26
|
+
| `zen memory export [f]` | The whole graph as one HTML page |
|
|
27
|
+
| `zen memory forget <id…>` | Remove nodes, their vectors and their files |
|
|
28
|
+
|
|
29
|
+
| Flag | Meaning |
|
|
30
|
+
| ----------------------- | -------------------------------------------- |
|
|
31
|
+
| `--project <name\|dir>` | Which project. Inferred from the directory |
|
|
32
|
+
| `--kind <name>` | Only this kind of node |
|
|
33
|
+
| `--audience <name>` | Only nodes committed under this label |
|
|
34
|
+
| `--files` | Only nodes that remember a file |
|
|
35
|
+
| `--stale` | Only nodes something has superseded |
|
|
36
|
+
| `--limit <n>` | Rows to list. Default 30 |
|
|
37
|
+
| `--out <file>` | Where `export` writes. Default `memory.html` |
|
|
38
|
+
| `--open` | Open the exported page |
|
|
39
|
+
| `--yes` | Do not ask before removing |
|
|
40
|
+
|
|
41
|
+
## It reads unmasked, on purpose
|
|
42
|
+
|
|
43
|
+
Memory is masked per agent: a node's `audience` decides who can see it. Recall
|
|
44
|
+
is masked, ranked and truncated as well, so what an agent gets is never the
|
|
45
|
+
whole picture.
|
|
46
|
+
|
|
47
|
+
This command ignores all of that. When the mask is the thing that is wrong, the
|
|
48
|
+
part that was hidden is exactly the part you need — and this is a person at a
|
|
49
|
+
terminal in the project directory, not an agent inside a run.
|
|
50
|
+
|
|
51
|
+
It also contacts no model. The store is opened without an embedder, so
|
|
52
|
+
inspection is free and works offline; the price is that `ls` filters by text
|
|
53
|
+
rather than by meaning.
|
|
54
|
+
|
|
55
|
+
## `export` is the one to reach for
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
zen memory export --open
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
One self-contained HTML file, three panes:
|
|
62
|
+
|
|
63
|
+
- **left** — every node, with a search box and filters for kind, audience,
|
|
64
|
+
files, superseded and unlinked
|
|
65
|
+
- **middle** — the graph as a Mermaid diagram, coloured by kind, with pan,
|
|
66
|
+
zoom, and a hop-focus selector for following one node's neighbourhood
|
|
67
|
+
- **right** — whatever you clicked, in full: the text, the metadata, the links,
|
|
68
|
+
and **the remembered file's actual content** — rendered as an image when it is
|
|
69
|
+
one, and as text otherwise
|
|
70
|
+
|
|
71
|
+
Clicking a node in either the list or the diagram selects it in both. Above 300
|
|
72
|
+
drawn nodes the diagram asks you to narrow it with the filters, because a
|
|
73
|
+
Mermaid graph that large is not a picture of anything.
|
|
74
|
+
|
|
75
|
+
It is a single file with no server behind it, so it attaches to a bug report.
|
|
76
|
+
The only network it does is fetching Mermaid; without that it degrades to a
|
|
77
|
+
working list and detail view.
|
|
78
|
+
|
|
79
|
+
## Forgetting
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
zen memory forget 01M1YPN0C8QRQS5BCZ03ABQ1G0
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Asks first, unless `--yes`, and refuses outright when there is no terminal to
|
|
86
|
+
ask at. A node, its vector row and its remembered file bytes go together —
|
|
87
|
+
there is one implementation of that, shared with the agent-facing tool, so the
|
|
88
|
+
three cannot drift apart.
|
|
89
|
+
|
|
90
|
+
**Forgetting is rarely the right correction.** The agents' own way to fix a
|
|
91
|
+
wrong memory is to commit the corrected node and supersede the old one, which
|
|
92
|
+
keeps the record of having been wrong. `zen memory ls --stale` lists what has
|
|
93
|
+
been superseded that way. Use `forget` for what should never have been stored —
|
|
94
|
+
a secret, a mistake, a pile of noise — not for what merely became untrue.
|
|
95
|
+
|
|
96
|
+
## Reading `stats`
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
nodes 7
|
|
100
|
+
edges 6
|
|
101
|
+
superseded 1
|
|
102
|
+
files 1 · 640 B
|
|
103
|
+
embedding none — recall falls back to term overlap
|
|
104
|
+
vectors —
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`embedding none` is worth noticing: without an embedder, recall matches on
|
|
108
|
+
shared terms rather than on meaning, which works but is much blunter. Set
|
|
109
|
+
`memory.embedding` in `agents.yaml` to change it.
|
|
110
|
+
|
|
111
|
+
A vector count lower than the node count means some nodes were committed while
|
|
112
|
+
no embedder was configured, and they will only ever be found by term overlap
|
|
113
|
+
until they are re-embedded.
|