@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.
Files changed (36) hide show
  1. package/README.md +20 -0
  2. package/dist/banner.js +69 -47
  3. package/dist/commands/index.js +3 -0
  4. package/dist/commands/init.js +8 -4
  5. package/dist/commands/inspect.js +23 -5
  6. package/dist/commands/memory.d.ts +3 -0
  7. package/dist/commands/memory.js +375 -0
  8. package/dist/engine.js +15 -9
  9. package/dist/main.js +9 -0
  10. package/dist/podman.js +43 -9
  11. package/dist/projects.js +10 -2
  12. package/dist/scaffold.js +22 -1
  13. package/dist/tui/app.js +19 -11
  14. package/dist/tui/theme.d.ts +7 -0
  15. package/dist/tui/theme.js +18 -11
  16. package/package.json +2 -2
  17. package/templates/editor/.github/copilot-instructions.md +151 -43
  18. package/templates/editor/.github/prompts/review-project.prompt.md +5 -0
  19. package/templates/editor/.github/prompts/sync-with-spec.prompt.md +7 -4
  20. package/templates/editor/.github/skills/zen-cli/SKILL.md +3 -0
  21. package/templates/editor/.github/skills/zen-cli/references/memory.md +113 -0
  22. package/templates/editor/.github/skills/zen-cli/references/rag.md +2 -2
  23. package/templates/editor/.github/skills/zen-cli/references/run.md +3 -1
  24. package/templates/editor/.github/skills/zen-memory/SKILL.md +555 -0
  25. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +16 -16
  26. package/templates/editor/.github/skills/zen-rag-schema/SKILL.md +2 -2
  27. package/templates/editor/.vscode/settings.json +4 -2
  28. package/templates/parts/exa.prompt.md.tmpl +4 -0
  29. package/templates/parts/exa.spec.md.tmpl +2 -0
  30. package/templates/project/INSTRUCTIONS.md +8 -0
  31. package/templates/project/SPECIFICATION.md.tmpl +47 -0
  32. package/templates/project/agents/prompts/{default.md → default.md.tmpl} +4 -0
  33. package/templates/project/agents.yaml.tmpl +4 -0
  34. package/templates/project/gitignore +7 -1
  35. package/templates/project/scripts/_setup.sh +101 -0
  36. 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 keeps the row's colour without its dimming. Nested
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 ? [{ key: `g:${b.name}`, label: `… ${gist}`, detail: '', past: true }] : []),
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: '', past: false });
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: true, children: r.label }), _jsx(Text, { dimColor: true, children: r.detail })] }, r.key))), _jsx(Text, { dimColor: true, children: '╰─' })] }, b.name));
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, dimColor: r.color === undefined, 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] }));
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
- 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, { dimColor: true, children: TITLE }), live ? _jsx(Text, { color: theme.warn, children: ` ${spin}` }) : null, _jsx(Text, { dimColor: true, children: ` ${'─'.repeat(dashes)}` })] }), shown.map((row, i) => {
639
- // Not italic: dim italic on a dark terminal is the least
640
- // legible thing available, and this is meant to be read.
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, { dimColor: true, children: '│ ' }), _jsx(Text, { dimColor: true, bold: head !== null, children: head ? head[1] : row })] }, i));
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
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, dimColor: true, children: agent }), busy ? (_jsxs(Text, { 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
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'}`
@@ -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
- // asides are drawn dim, and only the few things that must stand out —
8
- // the person's own turn, the agent name, a warning, an error — take a colour.
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
- // Dim alone, never dim *and* `gray`: bright black is already the
26
- // faintest colour a terminal has, and dimming it again puts the bulk
27
- // of the transcript a step from unreadable.
28
- tool: { dim: true },
29
- note: { color: 'cyan', dim: true },
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
- lanes: ['magenta', 'cyan', 'green', 'yellow', 'blue', 'red'],
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: { dim: true },
45
- note: { color: 'blue', dim: true },
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: ['magenta', 'blue', 'green', 'red'],
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.11",
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.11",
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, memory, whatever the agent wrote
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. A
1035
- **scope** is a namespace string; agents bound to the same scope share what is
1036
- in it, and the default scope is the agent's own — so memory is private until
1037
- something says otherwise. Recall is automatic: matches for the current input
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 a session-level facility rather than an `agents.yaml` key: `zen run`
1043
- binds each session's store under `sessions/`, so memory travels with the
1044
- session and is not part of this repository.
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 answer, stderr
1605
- is the narration**; every command takes `--json`. Exit codes: `0` ok, `1` failed,
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 | Change this |
1678
- | ------------------------------------------ | ------------------------------------------------------------------- |
1679
- | Wrong tone, wrong format, wrong length | `INSTRUCTIONS.md` (all agents) or the agent prompt |
1680
- | Says something forbidden | `INSTRUCTIONS.md` prohibition, stated specifically |
1681
- | Ignores a rule that only applies sometimes | Move the rule into a skill with a sharp description |
1682
- | Never loads the skill it should | The skill's `description`; or `preload` it |
1683
- | Loads too much, answers slowly | `allow:`, `maxIndexEntries:`, or `discovery: search` |
1684
- | Invents a number | A skill holding the figure, or a script that computes it — §3.4.1 |
1685
- | A skill in the catalog is never offered | It is a bare `<name>.md`; move it to `<name>/SKILL.md` — §3.4 |
1686
- | Cannot find a file its own skill names | Absolute `/skills/<name>/…` path, and `sandbox:*` — §3.4.1 |
1687
- | Rewrites a whole file to change one line | A prompt line preferring `apply_patch` — §3.6 |
1688
- | Edits files it should only be reading | Subtract the mutating tools, or `zen run --read-only` |
1689
- | Cannot run the build or the tests | Grant `sandbox:*`; add the toolchain to `sandbox/Dockerfile` |
1690
- | Installs the same packages on every run | Put them in `sandbox/Dockerfile`, or `sandbox.persist: true` — §3.7 |
1691
- | Answers from stale knowledge of the world | Grant `web_search` + `web_read`, and say when — §3.8 |
1692
- | Cites a page it only saw the excerpt of | A prompt line: `web_read` before quoting — §3.8 |
1693
- | Every web call refuses | No Exa key: `zen key add exa` — `zen check` warns — §3.8 |
1694
- | A model or embedder refuses every call | `zen models test <ref>` — if `blocked`, `zen models pick` — §8 |
1695
- | Answers instead of routing | Router prompt prohibition; check `handoffs:` |
1696
- | Routes to the wrong specialist | The target agents' `description:` fields |
1697
- | Gets stuck in the agent it routed to | The target needs a handoff back — §6.3 |
1698
- | Loses a detail after a handoff | Say it in the handoff; check the collapse policy |
1699
- | Works through N independent items serially | `fork:` on that agent, and a prompt line — §6.4 |
1700
- | Forks when the steps actually depend | Prompt line: branches cannot see each other |
1701
- | Slow and expensive on trivial cases | Demote that agent's model tier / reasoning effort |
1702
- | Fails only on genuinely hard cases | Promote that agent's tier, or split the hard path out |
1703
- | Shows no reasoning while it works | Turn summaries on for that model — §7.6 |
1704
- | Forgets across conversations | Continue the session rather than starting a new one |
1705
- | Breaks at load with a named path | Read the message — it names the exact key |
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. Consolidate any setup script that lives elsewhere into this shape,
102
- including one left at the project root.
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 returns 0; otherwise it redoes the work idempotently, writing
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.