@zenera/cli 1.1.19 → 1.1.21

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/dist/validate.js CHANGED
@@ -1,5 +1,5 @@
1
- import { EXA_GROUP, FileSkillProvider, SANDBOX_MOUNT, Sandbox, exaTools, projectRegistry, readProjectConfig, sandboxTools, selectTools, workspaceTools, } from '@zenera/neo';
2
- import { existsSync, mkdtempSync, readdirSync, realpathSync, rmSync, statSync } from 'node:fs';
1
+ import { EXA_GROUP, FileSkillProvider, SANDBOX_MOUNT, Sandbox, exaTools, frontmatter, projectRegistry, readProjectConfig, sandboxTools, selectTools, toList, workspaceTools, } from '@zenera/neo';
2
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
5
5
  import { auditModels, credentialFor } from "./audit.js";
@@ -7,6 +7,7 @@ import { resolveBuild } from "./image.js";
7
7
  import { SHAPES, envNames, form } from "./keys.js";
8
8
  import { probeModels } from "./liveness.js";
9
9
  import { BuildError, ensurePodmanReady } from "./podman.js";
10
+ import { staleShared } from "./scaffold.js";
10
11
  // Mirrors the loader's own constants (`packages/neo/src/project/load.ts`).
11
12
  // Duplicated rather than exported, because a check that agreed with the loader
12
13
  // by construction could not report that the two had diverged.
@@ -14,23 +15,39 @@ const CONFIG_NAMES = ['agents.yaml', 'agents.yml', 'agents/agents.yaml', 'agents
14
15
  const AGENTS_DIR = 'agents';
15
16
  const HOUSE_RULES = 'agents/instructions.md';
16
17
  const INSTRUCTIONS_SUFFIX = '-instructions.md';
18
+ /** The only place memory is explained to a model; the runtime composes none of it. */
19
+ const MEMORY_RULES = 'agents/memory-instructions.md';
20
+ /** The only place forking is explained to a model; the same holds. */
21
+ const FORK_RULES = 'agents/fork-instructions.md';
22
+ /** The conditioned documents `zen` ships, which land whether or not they apply. */
23
+ const OURS = new Set([MEMORY_RULES, FORK_RULES]);
17
24
  const PROMPTS_DIR = 'agents/prompts';
18
25
  const SKILLS_DIR = 'agents/skills';
19
26
  const SKILL_FILE = 'SKILL.md';
20
27
  const ASSETS_DIR = 'assets';
21
28
  /** What `allow:` and `preload:` accept, so a skill outside it cannot be named. */
22
29
  const REFERABLE = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
30
+ /** What `requires:` may name. Mirrors `CAPABILITIES` in the loader. */
31
+ const CAPABILITIES = {
32
+ fork: (spec) => Boolean(spec.fork),
33
+ memory: (spec) => Boolean(spec.memory),
34
+ 'memory-write': (spec) => spec.memory?.access === 'read-write' || spec.memory?.access === 'full',
35
+ 'memory-forget': (spec) => spec.memory?.access === 'full',
36
+ };
23
37
  /**
24
38
  * Mirrors `readHouseRules` in the loader, in the same order:
25
39
  * `agents/instructions.md`, then every `agents/<topic>-instructions.md` by
26
- * filename.
40
+ * filename — each with the `requires:` its frontmatter declared.
27
41
  */
28
42
  function houseRules(root) {
29
43
  const found = [];
30
44
  const take = (rel) => {
31
- if (existsSync(join(root, rel))) {
32
- found.push(rel);
45
+ const path = join(root, rel);
46
+ if (!existsSync(path)) {
47
+ return;
33
48
  }
49
+ const { data } = frontmatter(readFileSync(path, 'utf8'));
50
+ found.push({ rel, requires: toList(data.requires) });
34
51
  };
35
52
  take(HOUSE_RULES);
36
53
  const dir = join(root, AGENTS_DIR);
@@ -46,6 +63,15 @@ function houseRules(root) {
46
63
  }
47
64
  return found;
48
65
  }
66
+ /**
67
+ * The documents this agent's prompt actually gets. Unknown capabilities are
68
+ * reported once, against the file, rather than silently excluding every agent.
69
+ */
70
+ function rulesFor(rules, spec) {
71
+ return rules
72
+ .filter((r) => r.requires.every((cap) => CAPABILITIES[cap]?.(spec) ?? true))
73
+ .map((r) => r.rel);
74
+ }
49
75
  export async function validateProject(opts) {
50
76
  const root = resolve(opts.dir);
51
77
  const findings = [];
@@ -169,7 +195,7 @@ export async function validateProject(opts) {
169
195
  // Files the config and the conventions name
170
196
  // -----------------------------------------------------------------------
171
197
  const rules = houseRules(root);
172
- for (const rel of rules) {
198
+ for (const { rel, requires } of rules) {
173
199
  record(rel, 'house rules — prepended to every agent prompt', 'file', false);
174
200
  if (empty(join(root, rel))) {
175
201
  add({
@@ -180,6 +206,31 @@ export async function validateProject(opts) {
180
206
  fix: 'write the rules that hold whichever agent is answering, or delete it',
181
207
  });
182
208
  }
209
+ for (const cap of requires) {
210
+ if (!(cap in CAPABILITIES)) {
211
+ add({
212
+ severity: 'error',
213
+ code: 'rules.requires.unknown',
214
+ where: rel,
215
+ message: `\`requires: ${cap}\` names no capability, so the loader refuses the ` +
216
+ 'project',
217
+ fix: `use one of: ${Object.keys(CAPABILITIES).join(', ')}`,
218
+ });
219
+ }
220
+ }
221
+ // Ours are copied into every project whether or not it uses the
222
+ // capability, so an inert one says nothing about the project. One the
223
+ // project wrote is different: it was written to be read.
224
+ const reached = config.agents.some((a) => rulesFor([{ rel, requires }], a).length > 0);
225
+ if (requires.length && !reached && !OURS.has(rel)) {
226
+ add({
227
+ severity: 'note',
228
+ code: 'rules.unreached',
229
+ where: rel,
230
+ message: `no agent has ${requires.join(' and ')}, so this document reaches no prompt`,
231
+ fix: 'relax `requires:`, or give an agent the capability it is written for',
232
+ });
233
+ }
183
234
  }
184
235
  if (rules.length === 0) {
185
236
  record(HOUSE_RULES, 'house rules — prepended to every agent prompt', 'file', false);
@@ -191,6 +242,61 @@ export async function validateProject(opts) {
191
242
  'prompt alone, with nothing shared between them',
192
243
  });
193
244
  }
245
+ // The runtime composes the preference block and nothing else, so this file
246
+ // is the whole of what an agent is told about memory. Without it the tools
247
+ // are still granted and still callable, with only their schemas to go on -
248
+ // which is not a project that works less well, it is one that will write
249
+ // the graph wrong and cannot be told otherwise.
250
+ if (config.agents.some((a) => a.memory) || config.memory) {
251
+ const path = join(root, MEMORY_RULES);
252
+ if (!existsSync(path) || empty(path)) {
253
+ add({
254
+ severity: 'error',
255
+ code: 'memory.uninstructed',
256
+ where: MEMORY_RULES,
257
+ message: 'memory is on, but nothing tells an agent how to use it — the memory_* ' +
258
+ 'tools are granted with only their schemas to explain them',
259
+ fix: 'zen check --fix, which writes the current rules back',
260
+ });
261
+ }
262
+ }
263
+ // The same again for forking, and for the same reason: the runtime used to
264
+ // compose this block and no longer does, so the file is the whole of what
265
+ // an agent is told about what survives a join.
266
+ if (config.agents.some((a) => a.fork)) {
267
+ const path = join(root, FORK_RULES);
268
+ if (!existsSync(path) || empty(path)) {
269
+ add({
270
+ severity: 'error',
271
+ code: 'fork.uninstructed',
272
+ where: FORK_RULES,
273
+ message: 'an agent can fork, but nothing tells it what a branch can and cannot do — ' +
274
+ 'the fork tool is granted with only its schema to explain it',
275
+ fix: 'zen check --fix, which writes the current rules back',
276
+ });
277
+ }
278
+ }
279
+ // Present is not the same as current. These three files are copies of ours
280
+ // and nobody is meant to be maintaining them, so bytes that differ are an
281
+ // older `zen`'s — prose about a runtime that has since moved, which reads
282
+ // exactly as authoritative as the version that is true. The frontmatter
283
+ // that conditions them arrived this way: every project scaffolded before it
284
+ // carries a copy that reaches every agent, memory or no memory.
285
+ for (const rel of staleShared(root)) {
286
+ // An empty one is already reported, as missing or as empty.
287
+ if (empty(join(root, rel))) {
288
+ continue;
289
+ }
290
+ add({
291
+ severity: 'warning',
292
+ code: 'rules.stale',
293
+ where: rel,
294
+ message: `${rel} is not the copy this \`zen\` ships — it came from an older version, ` +
295
+ 'or was edited in place, and either way it describes behaviour that may no ' +
296
+ 'longer be the behaviour',
297
+ fix: 'zen check --fix, which replaces it; rules of your own belong in a topic file beside it',
298
+ });
299
+ }
194
300
  // -----------------------------------------------------------------------
195
301
  // Agents
196
302
  // -----------------------------------------------------------------------
@@ -211,7 +317,7 @@ export async function validateProject(opts) {
211
317
  // against the real set rather than a list written down twice.
212
318
  const available = availableTools(root, config);
213
319
  for (const spec of config.agents) {
214
- agents.push(checkAgent(root, config, spec, entry, available, record, add));
320
+ agents.push(checkAgent(root, config, spec, entry, available, rules, record, add));
215
321
  }
216
322
  checkReturnPaths(config, add);
217
323
  // -----------------------------------------------------------------------
@@ -430,9 +536,9 @@ export function availableTools(root, config) {
430
536
  ...exaTools(),
431
537
  ];
432
538
  }
433
- function checkAgent(root, config, spec, entry, available, record, add) {
539
+ function checkAgent(root, config, spec, entry, available, rules, record, add) {
434
540
  const where = `agents.${spec.name}`;
435
- const instructions = [...houseRules(root)];
541
+ const instructions = rulesFor(rules, spec);
436
542
  // The role prompt: named by `system:`, or found by convention. A named one
437
543
  // that is not there is an error — the loader says so too. An absent
438
544
  // conventional one is not, because a project may keep everything it has to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/cli",
3
- "version": "1.1.19",
3
+ "version": "1.1.21",
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.19",
54
+ "@zenera/neo": "^1.1.21",
55
55
  "@anthropic-ai/sdk": "^0.120.0",
56
56
  "@google/genai": "^2.18.0",
57
57
  "@openrouter/sdk": "^1.2.80",
@@ -222,7 +222,8 @@ my-project/
222
222
  ├── agents.yaml who exists, what they may reach for
223
223
  ├── agents/
224
224
  │ ├── instructions.md house rules, prepended to every agent
225
- │ ├── memory-instructions.md more of them - any `<topic>-instructions.md`
225
+ │ ├── fork-instructions.md more of them - any `<topic>-instructions.md`
226
+ │ ├── memory-instructions.md and more - how the graph is used
226
227
  │ ├── tools-instructions.md and more - how tools are called
227
228
  │ ├── prompts/
228
229
  │ │ ├── intake.md one agent's own brief
@@ -588,6 +589,21 @@ when `memory:` is turned on and are deleted when it is turned off, which is a
588
589
  whole file rather than a section somebody has to find. Do not split by author,
589
590
  by date, or to keep a file short; that is what a skill is for.
590
591
 
592
+ A document about one capability can say so in frontmatter, and then reaches only
593
+ the agents that have it:
594
+
595
+ ```markdown
596
+ ---
597
+ requires: [fork]
598
+ ---
599
+ ```
600
+
601
+ All of the list has to hold. The vocabulary is closed - `fork`, `memory`,
602
+ `memory-write`, `memory-forget` - and anything else is a load error. There is no
603
+ `not`: a rule for agents _without_ a capability is a rule about the rest of the
604
+ project, which is what `agents/instructions.md` is for. No frontmatter means
605
+ unconditional, which is the usual case.
606
+
591
607
  Put in them only what is true for **every** agent:
592
608
 
593
609
  - identity and domain ("You work the property claims desk")
@@ -1299,7 +1315,8 @@ remembered file's path comes from `memory_load` and not from listing `/memory`,
1299
1315
  what a node must carry to be worth having, and that a correction is a
1300
1316
  `SUPERSEDES` edge rather than an edit. It is its own file rather than a section of
1301
1317
  `agents/instructions.md` because it arrives with the `memory:` block and leaves
1302
- with it.
1318
+ with it - and it carries `requires: [memory]`, so an agent without the store
1319
+ never reads a word of it.
1303
1320
 
1304
1321
  **Leave that file as a pure copy.** Anything this project decides for itself -
1305
1322
  which audience holds what, what must never be written down, what an agent is
@@ -1483,11 +1500,15 @@ agent's prompt which one this work wants:
1483
1500
  lookups.
1484
1501
 
1485
1502
  Declaring `fork:` only makes the tool available. What a fork costs and what
1486
- survives the join is explained to the model by the runtime, in the system prompt
1487
- of every agent holding the key. What is left to you is _when_, in the terms of
1488
- this domain - _"When the request covers more than one region, fork one branch
1489
- per region and merge their findings"_ - or a weaker model will work through the
1490
- list serially and never call it.
1503
+ survives the join is in `agents/fork-instructions.md`, which carries
1504
+ `requires: [fork]` and so reaches exactly the agents holding the key. That file
1505
+ is `zen`'s, not yours - `zen check --fix` replaces it, editing it or carrying an
1506
+ older copy is reported as `rules.stale`, and deleting it leaves your forking
1507
+ agents with the tool schema alone (`fork.uninstructed`). What is
1508
+ left to you is _when_, in the terms of this domain - _"When the request covers
1509
+ more than one region, fork one branch per region and merge their findings"_ - or
1510
+ a weaker model will work through the list serially and never call it. Project
1511
+ policy on forking goes in a topic file of your own beside it.
1491
1512
 
1492
1513
  ### 6.5 Termination
1493
1514
 
@@ -1918,7 +1939,10 @@ Before finishing any change here:
1918
1939
  agent-specific
1919
1940
  - [ ] Nothing duplicated between a house-rules file and an agent prompt
1920
1941
  - [ ] Each `agents/<topic>-instructions.md` is about one capability, and is true
1921
- of every agent
1942
+ of every agent it reaches - `requires:` if it is about one of them
1943
+ - [ ] If any agent has `fork:`, `agents/fork-instructions.md` is present - §6.4
1944
+ - [ ] No `rules.stale`: the three files that are `zen`'s rather than yours say
1945
+ what this version of the runtime does, and `zen check --fix` replaces them
1922
1946
  - [ ] Every tool and agent referenced by its exact name
1923
1947
  - [ ] Failure paths stated for every instruction that can fail
1924
1948
  - [ ] No facts, rates or figures embedded in a prompt
@@ -2031,6 +2055,7 @@ candidates; these are the judgements to make about each one)**
2031
2055
  | Loses a detail after a handoff | Say it in the handoff; check the collapse policy |
2032
2056
  | Works through N independent items serially | `fork:` on that agent, and a prompt line - §6.4 |
2033
2057
  | Forks when the steps actually depend | Prompt line: branches cannot see each other |
2058
+ | Forks a sequence, or loses a branch's working | `agents/fork-instructions.md` is missing or empty - §6.4 |
2034
2059
  | Slow and expensive on trivial cases | Demote that agent's model tier / reasoning effort |
2035
2060
  | Fails only on genuinely hard cases | Promote that agent's tier, or split the hard path out |
2036
2061
  | Shows no reasoning while it works | Turn summaries on for that model - §7.6 |
@@ -21,7 +21,8 @@ What it writes:
21
21
  zenera.json { version, name }
22
22
  agents.yaml the configuration
23
23
  agents/instructions.md house rules, prepended to every agent
24
- agents/memory-instructions.md more of them, about the memory store
24
+ agents/fork-instructions.md more of them, for agents that can fork
25
+ agents/memory-instructions.md more of them, for agents with the memory store
25
26
  agents/tools-instructions.md more of them, about how tools are called
26
27
  agents/prompts/default.md the default agent's prompt
27
28
  agents/skills/ empty, for skills
@@ -31,13 +32,16 @@ sessions/ empty
31
32
  .env this project's environment, git-ignored
32
33
  .gitignore
33
34
  .vscode/settings.json editor files
35
+ .vscode/agents.schema.json what validates agents.yaml as you type it
34
36
  .github/ copilot instructions, prompts, this skill
35
37
  ```
36
38
 
37
39
  More house rules can be added by hand: any `agents/<topic>-instructions.md` is
38
- read too, in filename order, and prepended to every agent. A capability that
39
- needs standing rules - memory, say - gets its own file rather than another
40
- section of `agents/instructions.md`. Memory's are not written from scratch:
40
+ read too, in filename order, and prepended to every agent - or, with
41
+ `requires: [<capability>]` in its frontmatter, to every agent that has the
42
+ capability it is about. A capability that needs standing rules - memory, say -
43
+ gets its own file rather than another section of `agents/instructions.md`.
44
+ Memory's are not written from scratch:
41
45
  `agents/memory-instructions.md` is the same file as
42
46
  `.github/skills/zen-memory/references/memory-instructions.md`, which init puts
43
47
  in the project as well, so a copy that was lost or went stale is restored rather
@@ -46,9 +50,16 @@ which of the two it is, and `… fix` does the copy.
46
50
 
47
51
  The project's own files are never overwritten - `--force` is what allows
48
52
  writing into an occupied directory, and the files already there stay. The
49
- editor files are the exception: `.vscode/settings.json` and the `.github/` tree
50
- are ours and are replaced on every `init` and every `zen open`, so edits to them
51
- do not survive.
53
+ editor files are the exception: `.vscode/` and the `.github/` tree are ours and
54
+ are replaced on every `init` and every `zen open`, so edits to them do not
55
+ survive.
56
+
57
+ `.vscode/agents.schema.json` describes `agents.yaml` to the editor, so an
58
+ unknown key is underlined where it is typed and every key carries its
59
+ documentation on hover. It needs the `redhat.vscode-yaml` extension, which
60
+ `.vscode/extensions.json` recommends. It is an editor aid and nothing else: it
61
+ is rewritten from the version of `zen` in hand, and `zen check` is still what
62
+ says whether a project loads.
52
63
 
53
64
  The default agent gets the file tools and a sandboxed shell, plus `exa:*` when
54
65
  the keyring holds an Exa key. It also gets memory: the store is scaffolded in
@@ -102,7 +113,7 @@ The editor is chosen in this order: `--editor`, `$ZENERA_EDITOR`, the editor
102
113
  this terminal belongs to, `$VISUAL` or `$EDITOR`, a known editor on `PATH` or
103
114
  installed, then the platform opener.
104
115
 
105
- Opening refreshes the editor files (`.vscode/settings.json` and the `.github/`
116
+ Opening refreshes the editor files (`.vscode/` and the `.github/`
106
117
  tree) in the directory being opened. VS Code and its forks are launched with
107
118
  `--disable-workspace-trust`, so the settings written there apply to the new
108
119
  window immediately rather than after a prompt.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: zen-memory
3
- description: How agent memory is organised and how to configure it in `agents.yaml` - the `memory:` block, per-agent `access`/`sees`/`writes`/`autoRecall`, the four `memory_*` tools, kinds and relations, how recall ranks and stitches a subgraph, and how to design a memory strategy for a project. Includes the usage rules every memory-enabled project must carry in `agents/memory-instructions.md` (references/memory-instructions.md) and the script that says whether that copy is still current (scripts/check-instructions.sh), what to commit and what never to, how to keep a working file under `/memory` and re-run it later, and how to put memory in front of a `zen rag schema` or `zen rag docs` index so a search that already succeeded once is not paid for again.
3
+ description: How agent memory is organised and how to configure it in `agents.yaml` - the `memory:` block, per-agent `access`/`sees`/`writes`/`autoRecall`, the four `memory_*` tools, kinds and relations, how recall ranks and stitches a subgraph, and how to design a memory strategy for a project. Includes the default usage rules every memory-enabled project must carry in `agents/memory-instructions.md` (references/memory-instructions.md), the script that says whether that copy is still current (scripts/check-instructions.sh), where a project's own customisations go instead (`agents/memory-policy-instructions.md`, under `requires: [memory]`), what to commit and what never to, how to keep a working file under `/memory` and re-run it later, and how to put memory in front of a `zen rag schema` or `zen rag docs` index so a search that already succeeded once is not paid for again.
4
4
  ---
5
5
 
6
6
  # Memory
@@ -112,10 +112,14 @@ asked for; counting that as use would poison recency ranking. Commit is one
112
112
  call because a remembered thing is a subgraph, and building it with three calls
113
113
  leaves the graph half-formed when the model stops early.
114
114
 
115
- The system prompt already explains all of this to the agent - how to read a
116
- recollection, when committing is worthwhile, why correction is a new node. That
117
- text is **derived from `access`**, so an agent that cannot write is never told
118
- how to. Do not restate it in a prompt; state project-specific policy instead.
115
+ `agents/memory-instructions.md` already explains all of this to the agent - how
116
+ to read a recollection, when committing is worthwhile, why correction is a new
117
+ node. It is house rules under `requires: [memory]`, so it reaches exactly the
118
+ agents that have the store and no others, and it gates its own advice on the
119
+ tools the reader actually holds. The runtime adds the preference block and
120
+ nothing else. Do not restate any of it in a prompt; state project-specific
121
+ policy instead, and keep the copy byte-identical to the reference so
122
+ `check-instructions.sh` stays meaningful.
119
123
 
120
124
  ## How it is organised
121
125
 
@@ -328,6 +332,13 @@ Commit one only when the user's own words generalise: "always", "from now on",
328
332
  instruction, and a graph full of false preferences is a system prompt nobody
329
333
  wrote.
330
334
 
335
+ What the user states about themselves is the same kind of node, for the same
336
+ reason - the name to address them by, their role, their timezone, which of
337
+ several environments is theirs. Asking again next run is exactly what the block
338
+ exists to prevent. Credentials and secrets are not preferences, however they
339
+ were offered; `agents/memory-policy-instructions.md` is where a project says so
340
+ in its own terms.
341
+
331
342
  ## Remembering files, and running them again
332
343
 
333
344
  The workspace is disposable - it may be a container about to be discarded - so a
@@ -501,8 +512,8 @@ An index used by a project needs a skill in that project describing it - see
501
512
  index** belongs in that skill, because it is the same subject and it is read at
502
513
  the moment the index is. Policy that holds whatever the agent is doing -
503
514
  audiences, what must never be written down - goes in
504
- `agents/memory-policy-instructions.md` instead, which every agent gets whether
505
- it loads the skill or not.
515
+ `agents/memory-policy-instructions.md` instead, which every agent with the store
516
+ gets whether it loads the skill or not.
506
517
 
507
518
  ```md
508
519
  - Recalled routes are a shortcut, not authority. Confirm with `list_api --name`
@@ -559,11 +570,42 @@ prepended to every agent, in filename order, so this belongs in its own document
559
570
  rather than as another section of `agents/instructions.md` - it arrives with
560
571
  memory and it leaves with it.
561
572
 
562
- **Keep that file a pure copy.** Project-specific policy - audiences, what must
563
- never be written down, what an agent commits at the end of a job - goes in
564
- `agents/memory-policy-instructions.md`, which filename order puts directly after
565
- it. Then keeping up with a changed reference is the same one command again,
566
- rather than a hand-patch around prose that has to be preserved.
573
+ ### Two files, and which is which
574
+
575
+ | File | Is | Owned by |
576
+ | -------------------------------------- | ------------------------------------------------------------------ | ----------- |
577
+ | `agents/memory-instructions.md` | the DEFAULT rules - how the store works, and how any agent uses it | this skill |
578
+ | `agents/memory-policy-instructions.md` | this PROJECT's customisations on top of them | the project |
579
+
580
+ **Keep the first a pure copy.** It is predefined: it describes this version of
581
+ the runtime, not this project, and `check-instructions.sh` compares it byte for
582
+ byte. Anything edited into it is lost the next time the reference changes.
583
+
584
+ Everything a project decides for itself goes in the second - audiences, what
585
+ must never be written down, what an agent commits at the end of a job, which
586
+ index a recalled route is checked against. Filename order puts it directly
587
+ after the defaults, so it reads as the exceptions to them. Then keeping up with
588
+ a changed reference is the same one command again, rather than a hand-patch
589
+ around prose that has to be preserved.
590
+
591
+ **It must open with the same frontmatter**, or it is prepended to every agent in
592
+ the project, including the ones with no store to apply it to:
593
+
594
+ ```md
595
+ ---
596
+ requires: [memory]
597
+ ---
598
+
599
+ # Memory policy
600
+
601
+ - Never commit a customer name or an account id; a `fact` says which tenant
602
+ shape it was, never whose.
603
+ - ...
604
+ ```
605
+
606
+ `requires: [memory]` is what delivers a topic file on the capability it is
607
+ about. Use `requires: [memory, memory-write]` for a rule only a committing agent
608
+ can act on.
567
609
 
568
610
  ## Inspecting and repairing it
569
611
 
@@ -635,7 +677,8 @@ agents:
635
677
 
636
678
  - [ ] `.github/skills/zen-memory/scripts/check-instructions.sh` exits zero, so
637
679
  `agents/memory-instructions.md` is the reference byte for byte - and
638
- project policy lives in `agents/memory-policy-instructions.md`.
680
+ project policy lives in `agents/memory-policy-instructions.md`, opening
681
+ with `requires: [memory]`.
639
682
  - [ ] `memory.embedding` is set, and was set before the graph had content.
640
683
  - [ ] Every agent that should learn has a `memory:` binding - the top-level
641
684
  block alone enables nothing.
@@ -653,23 +696,24 @@ agents:
653
696
 
654
697
  ## When it goes wrong
655
698
 
656
- | Symptom | Cause |
657
- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
658
- | The `memory_*` tools are missing | No `memory:` on the agent. They come from the binding, never from `tools:` |
659
- | `zen memory` says the project has none | Neither a `memory:` block nor an agent binding - nothing is opened and no directory is made |
660
- | Recall finds a memory only when reworded | No embedder. `zen memory stats` says `embedding none` |
661
- | Recall finds nothing after changing the embedder | Refused rather than mixed - a manifest records the model. Change it back, or start a new memory |
662
- | Vectors fewer than nodes | Some nodes were committed with no embedder; they are only reachable by term overlap |
663
- | The graph fills with restated requests | The commit rule is not in a prompt or skill. State the "would a later run redo this" test |
664
- | An agent greps `/memory` or reads `graph.json` | The house-rules block is missing from `agents/memory-instructions.md` - the system prompt never forbids it |
665
- | A recalled fact cannot be checked or continued | Nodes were committed with no provenance. The block's "say where it came from" rule is what prevents it |
666
- | Recall returns pointers and the run re-reads it all | Content was committed as `text` alone. A node's text describes and locates; the content belongs in a file |
667
- | A wrong memory keeps coming back | It was edited instead of superseded, or superseded in the wrong direction - the **new** node is the source |
668
- | An agent cannot see a node you can | Its `audience` is not in that agent's `sees`. Invisible and missing are the same thing, on purpose |
669
- | "this agent cannot remember files" | The agent has no workspace, so a path cannot be resolved. Commit without `file` |
670
- | `error: outside the workspace: /tmp/…` | The file is real to the shell and outside the only tree memory reads. Write it under `/workspace` |
671
- | Long content arrives as `text`, never as a `file` | The agent has no tool that writes to the workspace, so it has no path to name |
672
- | A remembered file will not run | It is not self-contained - it referenced the workspace it was written in |
673
- | Recollections are large and unhelpful | `autoRecall.limit` is too high, or node texts describe answers rather than questions |
674
- | The model recalls a route the API no longer has | Nothing re-checks memory against a rebuilt index. Supersede, and require verification |
675
- | A second run of the same project refuses to start | The directory lock. A lock whose process is gone is stale and is taken over |
699
+ | Symptom | Cause |
700
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
701
+ | The `memory_*` tools are missing | No `memory:` on the agent. They come from the binding, never from `tools:` |
702
+ | `zen memory` says the project has none | Neither a `memory:` block nor an agent binding - nothing is opened and no directory is made |
703
+ | Recall finds a memory only when reworded | No embedder. `zen memory stats` says `embedding none` |
704
+ | Recall finds nothing after changing the embedder | Refused rather than mixed - a manifest records the model. Change it back, or start a new memory |
705
+ | Vectors fewer than nodes | Some nodes were committed with no embedder; they are only reachable by term overlap |
706
+ | The graph fills with restated requests | The commit rule is not in a prompt or skill. State the "would a later run redo this" test |
707
+ | Runs do real work and the graph stays empty | The agent is bound `read`, or nothing names the moments: empty recall then work, before an answer, a handoff or a fork's return |
708
+ | An agent greps `/memory` or reads `graph.json` | The house-rules block is missing from `agents/memory-instructions.md` - the system prompt never forbids it |
709
+ | A recalled fact cannot be checked or continued | Nodes were committed with no provenance. The block's "say where it came from" rule is what prevents it |
710
+ | Recall returns pointers and the run re-reads it all | Content was committed as `text` alone. A node's text describes and locates; the content belongs in a file |
711
+ | A wrong memory keeps coming back | It was edited instead of superseded, or superseded in the wrong direction - the **new** node is the source |
712
+ | An agent cannot see a node you can | Its `audience` is not in that agent's `sees`. Invisible and missing are the same thing, on purpose |
713
+ | "this agent cannot remember files" | The agent has no workspace, so a path cannot be resolved. Commit without `file` |
714
+ | `error: outside the workspace: /tmp/…` | The file is real to the shell and outside the only tree memory reads. Write it under `/workspace` |
715
+ | Long content arrives as `text`, never as a `file` | The agent has no tool that writes to the workspace, so it has no path to name |
716
+ | A remembered file will not run | It is not self-contained - it referenced the workspace it was written in |
717
+ | Recollections are large and unhelpful | `autoRecall.limit` is too high, or node texts describe answers rather than questions |
718
+ | The model recalls a route the API no longer has | Nothing re-checks memory against a rebuilt index. Supersede, and require verification |
719
+ | A second run of the same project refuses to start | The directory lock. A lock whose process is gone is stale and is taken over |
@@ -101,7 +101,8 @@ show_diff() {
101
101
  fix() {
102
102
  cp "$REFERENCE" "$COPY"
103
103
  echo "wrote: $COPY"
104
- echo "note: project policy belongs in agents/memory-policy-instructions.md"
104
+ echo "note: project policy belongs in agents/memory-policy-instructions.md,"
105
+ echo " which must open with '---' / 'requires: [memory]' / '---'"
105
106
  }
106
107
 
107
108
  case "${1:-status}" in