@zenera/cli 1.1.16 → 1.1.18

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 (71) hide show
  1. package/README.md +110 -19
  2. package/dist/banner.d.ts +19 -0
  3. package/dist/banner.js +12 -9
  4. package/dist/catalog.js +19 -20
  5. package/dist/command.d.ts +3 -0
  6. package/dist/commands/check.js +5 -1
  7. package/dist/commands/index.js +4 -1
  8. package/dist/commands/init.js +42 -7
  9. package/dist/commands/inspect.js +77 -13
  10. package/dist/commands/key.js +49 -1
  11. package/dist/commands/memory.js +16 -3
  12. package/dist/commands/meta.d.ts +3 -0
  13. package/dist/commands/meta.js +596 -0
  14. package/dist/commands/run.js +64 -35
  15. package/dist/engine.d.ts +15 -0
  16. package/dist/engine.js +26 -1
  17. package/dist/env.d.ts +20 -0
  18. package/dist/env.js +65 -0
  19. package/dist/home.d.ts +2 -0
  20. package/dist/home.js +2 -0
  21. package/dist/keys.d.ts +2 -0
  22. package/dist/keys.js +2 -0
  23. package/dist/main.js +1 -1
  24. package/dist/meta.d.ts +221 -0
  25. package/dist/meta.js +695 -0
  26. package/dist/podman.js +64 -2
  27. package/dist/projects.d.ts +9 -1
  28. package/dist/projects.js +19 -2
  29. package/dist/resolve.js +7 -1
  30. package/dist/sandbox.d.ts +11 -0
  31. package/dist/sandbox.js +24 -4
  32. package/dist/scaffold.d.ts +6 -0
  33. package/dist/scaffold.js +56 -15
  34. package/dist/session.d.ts +10 -0
  35. package/dist/session.js +19 -1
  36. package/dist/term.d.ts +7 -0
  37. package/dist/term.js +8 -1
  38. package/dist/tui/app.js +33 -5
  39. package/dist/tui/wrap.d.ts +1 -1
  40. package/dist/tui/wrap.js +1 -1
  41. package/dist/validate.js +57 -15
  42. package/package.json +3 -2
  43. package/templates/editor/.github/copilot-instructions.md +313 -94
  44. package/templates/editor/.github/prompts/project-review.prompt.md +74 -0
  45. package/templates/editor/.github/prompts/spec-apply-feedback.prompt.md +142 -0
  46. package/templates/editor/.github/prompts/spec-sync-project.prompt.md +392 -0
  47. package/templates/editor/.github/skills/zen-cli/references/check.md +6 -3
  48. package/templates/editor/.github/skills/zen-cli/references/frame.md +5 -3
  49. package/templates/editor/.github/skills/zen-cli/references/inspect.md +4 -3
  50. package/templates/editor/.github/skills/zen-cli/references/memory.md +14 -0
  51. package/templates/editor/.github/skills/zen-cli/references/projects.md +19 -2
  52. package/templates/editor/.github/skills/zen-cli/references/run.md +75 -16
  53. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +6 -0
  54. package/templates/editor/.github/skills/zen-memory/SKILL.md +153 -33
  55. package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +115 -0
  56. package/templates/editor/.github/skills/zen-review/SKILL.md +123 -0
  57. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +119 -0
  58. package/templates/editor/.github/skills/zen-review/scripts/review.sh +104 -0
  59. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +197 -0
  60. package/templates/editor/.github/skills/zen-spec-sync/scripts/snapshot.sh +200 -0
  61. package/templates/editor/.vscode/settings.json +44 -4
  62. package/templates/parts/memory-terms.yaml.tmpl +16 -0
  63. package/templates/parts/memory.yaml.tmpl +12 -0
  64. package/templates/project/SPECIFICATION.md.tmpl +32 -2
  65. package/templates/project/agents/memory-instructions.md +278 -0
  66. package/templates/project/agents.yaml.tmpl +12 -3
  67. package/templates/project/env +28 -0
  68. package/templates/project/gitignore +4 -4
  69. package/templates/editor/.github/prompts/review-project.prompt.md +0 -36
  70. package/templates/editor/.github/prompts/sync-with-spec.prompt.md +0 -205
  71. /package/templates/project/{INSTRUCTIONS.md → agents/instructions.md} +0 -0
package/README.md CHANGED
@@ -125,7 +125,7 @@ In the editor, describe the job in `SPECIFICATION.md`, then send this in the
125
125
  agent chat:
126
126
 
127
127
  ```
128
- /sync-with-spec
128
+ /spec-sync-project
129
129
  ```
130
130
 
131
131
  It creates the specialists, responsibilities, tool access and handoffs the job
@@ -145,7 +145,7 @@ zen run my-project # no prompt given: opens a full-screen terminal
145
145
  zen check my-project # validate the project and every file it names
146
146
  zen inspect # open the last run's report.html
147
147
  zen list --sessions # every project, its sessions and last run
148
- echo "triage this" | zen run my-project --quiet | jq
148
+ echo "triage this" | zen run my-project --json | jq
149
149
  ```
150
150
 
151
151
  Standing inside the project, the name is optional: a bare `zen run`, `zen check`
@@ -156,7 +156,7 @@ session and uses the current directory as the workspace, with write access.
156
156
  `--session`, `--workspace` and `--read-only` override that.
157
157
 
158
158
  To change what the system does, update `SPECIFICATION.md` and send
159
- `/sync-with-spec` again. The next section explains that workflow in detail.
159
+ `/spec-sync-project` again. The next section explains that workflow in detail.
160
160
 
161
161
  ## You write the specification; a coding agent writes the system
162
162
 
@@ -202,12 +202,15 @@ In the window that opens:
202
202
 
203
203
  1. **Edit `SPECIFICATION.md`** - what the system is for, which agents exist,
204
204
  what each may reach for, and what _done_ means.
205
- 2. **Open the chat panel and send `/sync-with-spec`.** It reads the
205
+ 2. **Open the chat panel and send `/spec-sync-project`.** It reads the
206
206
  specification and every file implementing it, works out the difference in
207
207
  both directions, and changes the smallest thing that closes each gap.
208
- 3. **Read `SPECIFICATION-FEEDBACK.md`**, which it writes for anything it could
209
- not do without guessing. Answer its questions by editing `SPECIFICATION.md`
210
- - not by editing prompts - and send `/sync-with-spec` again.
208
+ 3. **Answer `SPECIFICATION-FEEDBACK.md`**, which it writes for anything it could
209
+ not do without guessing. Every question carries two to four written-out
210
+ answers, each with a `[ ]` box: tick one, delete the ones you do not want, or
211
+ write your own in their place.
212
+ 4. **Send `/spec-apply-feedback`.** It folds the answers you chose into
213
+ `SPECIFICATION.md` - never into a prompt - and then `/spec-sync-project` again.
211
214
 
212
215
  Back in the terminal: `zen check`, then `zen run`.
213
216
 
@@ -218,15 +221,16 @@ They are prompt files under `.github/prompts/`, which VS Code and its forks
218
221
  offer as chat slash-commands. Both `zen init` and `zen open` write them fresh,
219
222
  so they never go stale - and edits to them do not survive.
220
223
 
221
- | In chat | Does |
222
- | ----------------- | ----------------------------------------------------------- |
223
- | `/sync-with-spec` | Makes every file match `SPECIFICATION.md`, both directions. |
224
- | `/review-project` | Reads the project as a reviewer would, and reports. |
225
- | `/new-agent` | Adds an agent - prompt, wiring and hand-offs. |
226
- | `/new-skill` | Adds a skill under `agents/skills/`. |
224
+ | In chat | Does |
225
+ | ---------------------- | ---------------------------------------------------------------- |
226
+ | `/spec-sync-project` | Makes every file match `SPECIFICATION.md`, both directions. |
227
+ | `/spec-apply-feedback` | Folds your answers in `SPECIFICATION-FEEDBACK.md` into the spec. |
228
+ | `/project-review` | Reads the project as a reviewer would, and reports. |
229
+ | `/new-agent` | Adds an agent - prompt, wiring and hand-offs. |
230
+ | `/new-skill` | Adds a skill under `agents/skills/`. |
227
231
 
228
232
  In an editor that does not support prompt files, paste the contents of
229
- `.github/prompts/sync-with-spec.prompt.md` into its chat instead - it is only a
233
+ `.github/prompts/spec-sync-project.prompt.md` into its chat instead - it is only a
230
234
  prompt. Alongside them, `.github/copilot-instructions.md` is the standing brief
231
235
  that explains this runtime to whatever agent is reading, and
232
236
  `.github/skills/zen-cli/` is the CLI's own reference for it.
@@ -236,7 +240,7 @@ that explains this runtime to whatever agent is reading, and
236
240
  ### The self-improving loop
237
241
 
238
242
  ```
239
- edit SPECIFICATION.md → /sync-with-spec → zen check → zen run
243
+ edit SPECIFICATION.md → /spec-sync-project → zen check → zen run
240
244
  ↑ ↓
241
245
  └───── refine the spec <-──── zen inspect <-───────┘
242
246
  ```
@@ -295,7 +299,7 @@ Read a codebase and leave a short written note about it in the workspace.
295
299
  - Every claim in it comes from a file that was actually read.
296
300
  ```
297
301
 
298
- Send `/sync-with-spec` in the editor's chat. It writes `agents.yaml` - who
302
+ Send `/spec-sync-project` in the editor's chat. It writes `agents.yaml` - who
299
303
  exists, and what each may reach for:
300
304
 
301
305
  ```yaml
@@ -378,20 +382,24 @@ is a command on their machine too.
378
382
  Prompts, agent wiring, skills and tool selections are all files - Markdown and
379
383
  YAML - rather than code buried inside an application. So the folder can be
380
384
  committed, copied to another machine, reviewed in a pull request, and handed to
381
- someone else who runs it with one command. Credentials live in `$HOME`, never in
382
- the project, so there is no secret to strip before sharing it.
385
+ someone else who runs it with one command. Model credentials live in `$HOME`,
386
+ never in the project, and anything the project itself needs goes in a
387
+ git-ignored `.env` - so there is no secret to strip before sharing it.
383
388
 
384
389
  ```
385
390
  my-project/
386
391
  SPECIFICATION.md what this is for - the intent
387
- INSTRUCTIONS.md house rules, prepended to every agent
388
392
  agents.yaml who exists, what they may reach for
389
393
  agents/
394
+ instructions.md house rules, prepended to every agent
395
+ <topic>-instructions.md more of them, one subject at a time
390
396
  prompts/<name>.md each agent's own brief
391
397
  skills/<name>/ knowledge loaded on demand, not always-on
392
398
  assets/ reference material, read-only at /assets
393
399
  sandbox/Dockerfile the container commands run in
394
400
  scripts/_setup.sh the one command that initialises the project
401
+ .spec-sync/ what the last sync with the spec applied
402
+ .env this project's environment - git-ignored
395
403
  sessions/ one workspace, memory and trajectory each
396
404
  <id>/
397
405
  workspace/ what the agents can read and write
@@ -457,6 +465,7 @@ remove resources beyond the current project.
457
465
  | `key` | The credential keyring - add, check, switch, remove. |
458
466
  | `models` | What this machine can use - list, search, test, pick. |
459
467
  | `run` | Runs the project - the TUI on a terminal, a single answer otherwise. |
468
+ | `meta` | Runs the meta agent over the project - the one that edits it. |
460
469
  | `inspect` | Opens or rebuilds a run's `report.html`. |
461
470
  | `memory` | What the agents remember - size, listing, one node, or a whole page. |
462
471
  | `check` | Validates the project and every file it names, and asks the models. |
@@ -531,6 +540,88 @@ an accident.
531
540
  | `4` | no usable credential |
532
541
  | `5` | sandbox unavailable |
533
542
 
543
+ ## The meta agent - the agent that works on the project
544
+
545
+ `zen run` runs the agents your project describes. `zen meta` runs an agent _over_
546
+ the project: a coding agent rooted at the project directory, which reads
547
+ `SPECIFICATION.md`, changes `agents.yaml`, the prompts and the skills, and runs
548
+ whatever commands it needs to check itself - on the keys already in your
549
+ keyring, and not on anybody's subscription. It is the loop your editor's chat
550
+ panel drives, as a command you can put in a script.
551
+
552
+ ```sh
553
+ zen meta run "what does this project do?"
554
+ zen meta run acme "review the last commit"
555
+ zen meta prompts # which /<name> prompts this project has
556
+ zen meta run /project-review # .github/prompts/project-review.prompt.md
557
+ zen meta run acme /spec-sync-project agents/triage.md
558
+ git diff | zen meta run "what broke?" --allow-tool read
559
+ zen meta run --dry-run "hello" # what would run, secrets masked
560
+ ```
561
+
562
+ Every prompt goes through `run`, whether you typed it or it is a file: the
563
+ alternative was a verb that appeared only sometimes, and a bare word after
564
+ `zen meta` that was a subcommand on Tuesdays and a question on Wednesdays.
565
+
566
+ It uses every tool without asking. The prompts it runs were written for an
567
+ editor, where reading files, writing them and running commands is the whole
568
+ point, and a `-p` run has nobody sitting there to answer the question - so the
569
+ alternative is not a safer run but a stalled one. `--allow-tool read`,
570
+ `--allow-tool 'shell(git:*)'` narrow it to what a particular run needs, and
571
+ `--ask` puts the question back. It is your own checkout and your own key either
572
+ way, so treat it as you would `git push`: fine on a project you own, worth
573
+ narrowing on one you do not.
574
+
575
+ The answer goes to stdout and the progress to stderr, like everything else here,
576
+ so `zen meta run "…" > out.md` keeps the answer alone.
577
+
578
+ Progress is a small window that rewrites itself rather than a transcript
579
+ scrolling past, because only the last few steps say what it is doing now. The
580
+ whole of it - the prompt, every tool call, the answer - is written as it happens
581
+ to `.tmp/logs/meta.<when>.log`, whose path is printed before the run starts, so
582
+ `tail -f` in another terminal follows a run already going. The answer alone is
583
+ kept beside it as `.tmp/logs/meta.<when>.md`, named again once it has printed -
584
+ a terminal turns that into something you click.
585
+
586
+ `zen meta run /<name>` is the part an editor cannot do for you from a script.
587
+ The prompts under `.github/prompts/` - `/spec-sync-project`, `/project-review` -
588
+ are what a chat panel offers as slash commands, and nothing outside an editor
589
+ reads them. zen reads one, drops the frontmatter and sends the body - so the
590
+ same prompt runs from the editor, from a terminal and from CI.
591
+
592
+ A terminal has no menu dropping down as you type a slash, so
593
+ `zen meta prompts` lists what this project holds, each with its description.
594
+ `zen meta run` with nothing to say asks the same question interactively.
595
+
596
+ ### Which model it runs on
597
+
598
+ ```sh
599
+ zen meta model # the whole chain, with the winner marked
600
+ zen meta model vertex/gemini-3.8-flash # for every project on this machine
601
+ zen meta model --local openai/gpt-5.6-sol # for this project only, via its .env
602
+ zen meta model --pick # choose from providers you hold a key for
603
+ ```
604
+
605
+ Highest first: `--model`, `ZENERA_META_MODEL` in the shell, the same variable in
606
+ the project's `.env`, `zen meta model`, then the project's `agents.yaml`
607
+ `model:`. When none of them answers, a key does: the best model zen knows of
608
+ for the first provider you hold one for, named on stderr as it starts, so a
609
+ fresh machine needs a key and nothing else.
610
+ The store is separate from `agents.yaml` on purpose - the model a coding agent
611
+ runs on is a personal choice about a tool, like a key, where `agents.yaml` is a
612
+ committed decision about the project's own agents.
613
+
614
+ Not every model works. The meta agent declares its tools in a form only the
615
+ reasoning APIs accept, so use a reasoning model - `gpt-5.6-sol`,
616
+ `claude-opus-5`, `gemini-3.8-flash`. Those three are also what it falls back
617
+ to, and what `zen meta model --pick` offers.
618
+
619
+ Setting one asks it a one-word question before storing it, so a model that will
620
+ not answer is refused with the provider's own words rather than at the start of
621
+ the next run. A typed prefix that names no provider - `vertes/...` - is caught
622
+ before that, because it would otherwise be sent whole to a vendor you never
623
+ named. `--force` stores a ref without asking.
624
+
534
625
  ## Credentials
535
626
 
536
627
  One keyring serves every provider, and a key goes in the same way whatever it
package/dist/banner.d.ts CHANGED
@@ -1,3 +1,19 @@
1
+ /** One per sub-brand, so two of them are told apart before the word is read. */
2
+ declare const ACCENTS: {
3
+ readonly orange: {
4
+ readonly face: "\u001B[1;38;5;208m";
5
+ readonly shade: "\u001B[0;38;5;130m";
6
+ };
7
+ readonly cyan: {
8
+ readonly face: "\u001B[1;38;5;51m";
9
+ readonly shade: "\u001B[0;38;5;37m";
10
+ };
11
+ readonly mint: {
12
+ readonly face: "\u001B[1;38;5;121m";
13
+ readonly shade: "\u001B[0;38;5;78m";
14
+ };
15
+ };
16
+ export type Hue = keyof typeof ACCENTS;
1
17
  export interface BannerText {
2
18
  /** drawn white */
3
19
  head: string;
@@ -5,6 +21,8 @@ export interface BannerText {
5
21
  accent: string;
6
22
  /** the line underneath, dim */
7
23
  subtitle: string;
24
+ /** which neon to use. Default: orange, the `zen` mark. */
25
+ hue?: Hue;
8
26
  }
9
27
  export interface PrintBannerOptions {
10
28
  /** Render the block face even when the terminal is narrower than it. */
@@ -14,4 +32,5 @@ export declare const NEO_BANNER: BannerText;
14
32
  export declare function bannerLines(text: BannerText, columns?: number): string[];
15
33
  /** Narration, and only for someone watching. */
16
34
  export declare function printBanner(text: BannerText, options?: PrintBannerOptions): void;
35
+ export {};
17
36
  //# sourceMappingURL=banner.d.ts.map
package/dist/banner.js CHANGED
@@ -1,5 +1,4 @@
1
- import { styleText } from 'node:util';
2
- import { dim, note } from "./term.js";
1
+ import { dim, note, styled } from "./term.js";
3
2
  // ---------------------------------------------------------------------------
4
3
  // The banner
5
4
  //
@@ -65,11 +64,14 @@ function big(word) {
65
64
  }
66
65
  /** A wordmark rather than a sentence, so the letters are set apart. */
67
66
  const spaced = (s) => [...s.toUpperCase()].join(' ');
68
- /** Whether anything should be coloured at all — `styleText`'s own answer. */
69
- const styling = () => styleText('dim', '.') !== '.';
70
67
  const RESET = '\u001b[0m';
71
68
  const HEAD = { face: '\u001b[1;38;5;231m', shade: '\u001b[0;38;5;244m' };
72
- const ACCENT = { face: '\u001b[1;38;5;208m', shade: '\u001b[0;38;5;130m' };
69
+ /** One per sub-brand, so two of them are told apart before the word is read. */
70
+ const ACCENTS = {
71
+ orange: { face: '\u001b[1;38;5;208m', shade: '\u001b[0;38;5;130m' },
72
+ cyan: { face: '\u001b[1;38;5;51m', shade: '\u001b[0;38;5;37m' },
73
+ mint: { face: '\u001b[1;38;5;121m', shade: '\u001b[0;38;5;78m' },
74
+ };
73
75
  /** Runs of stroke and runs of bevel, alternating; spaces stay unstyled. */
74
76
  const RUNS = /[█▀▄]+|[^█▀▄ ]+/g;
75
77
  const isStroke = (run) => /[█▀▄]/.test(run);
@@ -77,11 +79,11 @@ const isStroke = (run) => /[█▀▄]/.test(run);
77
79
  * Two tones per word — without the darker bevel the letter is a flat outline
78
80
  * and the shadow reads as part of the stroke.
79
81
  */
80
- const paint = (text, tone) => styling()
82
+ const paint = (text, tone) => styled()
81
83
  ? text.replace(RUNS, (run) => `${isStroke(run) ? tone.face : tone.shade}${run}${RESET}`)
82
84
  : text;
83
85
  /** The wordmark has no bevel to shade, so it is all face. */
84
- const flat = (text, tone) => styling() ? `${tone.face}${text}${RESET}` : text;
86
+ const flat = (text, tone) => styled() ? `${tone.face}${text}${RESET}` : text;
85
87
  export const NEO_BANNER = {
86
88
  head: 'Zenera',
87
89
  accent: 'Neo',
@@ -92,17 +94,18 @@ export function bannerLines(text, columns = process.stderr.columns || 80) {
92
94
  // Trimmed: the widest accent row is the banner's right edge, and a pad left
93
95
  // inside a styled string cannot be trimmed away later.
94
96
  const accent = big(text.accent).map((row) => row.trimEnd());
97
+ const tone = ACCENTS[text.hue ?? 'orange'];
95
98
  const margin = ' '.repeat(INDENT);
96
99
  const width = INDENT + head[0].length + GAP + Math.max(...accent.map((row) => row.length));
97
100
  if (width > columns) {
98
101
  return [
99
- `${margin}${flat(text.head.toUpperCase(), HEAD)} ${flat(text.accent.toUpperCase(), ACCENT)}`,
102
+ `${margin}${flat(text.head.toUpperCase(), HEAD)} ${flat(text.accent.toUpperCase(), tone)}`,
100
103
  `${margin}${dim(spaced(text.subtitle))}`,
101
104
  ];
102
105
  }
103
106
  const lines = [];
104
107
  for (let r = 0; r < HEIGHT; r++) {
105
- lines.push(`${margin}${paint(head[r], HEAD)}${' '.repeat(GAP)}${paint(accent[r], ACCENT)}`);
108
+ lines.push(`${margin}${paint(head[r], HEAD)}${' '.repeat(GAP)}${paint(accent[r], tone)}`);
106
109
  }
107
110
  // Rules on both sides, centred under the art: the subtitle is a caption,
108
111
  // and without them a short line of spaced capitals floats.
package/dist/catalog.js CHANGED
@@ -38,29 +38,28 @@ const CATALOG_VERSION = 1;
38
38
  */
39
39
  export const CURATED = {
40
40
  openai: [
41
- { id: 'gpt-4o-mini', roles: ['chat'], contextLength: 128_000 },
42
- { id: 'gpt-4o', roles: ['chat'], contextLength: 128_000 },
41
+ { id: 'gpt-5.6-luna', roles: ['chat'], contextLength: 1_050_000 },
42
+ { id: 'gpt-5.6-terra', roles: ['chat'], contextLength: 1_050_000 },
43
43
  { id: 'text-embedding-3-small', roles: ['embedding'], dimensions: 1536 },
44
44
  { id: 'text-embedding-3-large', roles: ['embedding'], dimensions: 3072 },
45
45
  ],
46
46
  anthropic: [
47
- { id: 'claude-haiku-4-5', roles: ['chat'], contextLength: 200_000 },
48
- { id: 'claude-sonnet-4-5', roles: ['chat'], contextLength: 200_000 },
47
+ { id: 'claude-sonnet-5', roles: ['chat'], contextLength: 1_000_000 },
48
+ { id: 'claude-opus-5', roles: ['chat'], contextLength: 1_000_000 },
49
49
  ],
50
50
  google: [
51
- { id: 'gemini-2.5-flash', roles: ['chat'], contextLength: 1_048_576 },
52
- { id: 'gemini-2.5-pro', roles: ['chat'], contextLength: 1_048_576 },
53
- { id: 'gemini-embedding-001', roles: ['embedding'], dimensions: 3072 },
51
+ { id: 'gemini-3.5-flash-lite', roles: ['chat'], contextLength: 1_048_576 },
52
+ { id: 'gemini-3.8-flash', roles: ['chat'], contextLength: 1_048_576 },
53
+ { id: 'gemini-embedding-2', roles: ['embedding'] },
54
54
  ],
55
55
  vertex: [
56
- { id: 'gemini-2.5-flash', roles: ['chat'], contextLength: 1_048_576 },
57
- { id: 'gemini-2.5-pro', roles: ['chat'], contextLength: 1_048_576 },
58
- { id: 'gemini-embedding-001', roles: ['embedding'], dimensions: 3072 },
59
- { id: 'text-embedding-005', roles: ['embedding'], dimensions: 768 },
56
+ { id: 'gemini-3.5-flash-lite', roles: ['chat'], contextLength: 1_048_576 },
57
+ { id: 'gemini-3.8-flash', roles: ['chat'], contextLength: 1_048_576 },
58
+ { id: 'gemini-embedding-2', roles: ['embedding'] },
60
59
  ],
61
60
  openrouter: [
62
- { id: 'openai/gpt-4o-mini', roles: ['chat'] },
63
- { id: 'anthropic/claude-haiku-4.5', roles: ['chat'] },
61
+ { id: 'openai/gpt-5.6-luna', roles: ['chat'], contextLength: 1_050_000 },
62
+ { id: 'anthropic/claude-sonnet-5', roles: ['chat'], contextLength: 1_000_000 },
64
63
  { id: 'openai/text-embedding-3-small', roles: ['embedding'], dimensions: 1536 },
65
64
  ],
66
65
  };
@@ -73,23 +72,23 @@ export const CURATED = {
73
72
  */
74
73
  export const PREFERRED = {
75
74
  openai: {
76
- chat: ['gpt-4o-mini', 'gpt-4o'],
75
+ chat: ['gpt-5.6-luna', 'gpt-5.6-terra'],
77
76
  embedding: ['text-embedding-3-small', 'text-embedding-3-large'],
78
77
  },
79
78
  anthropic: {
80
- chat: ['claude-haiku-4-5', 'claude-sonnet-4-5'],
79
+ chat: ['claude-sonnet-5', 'claude-opus-5'],
81
80
  embedding: [],
82
81
  },
83
82
  google: {
84
- chat: ['gemini-2.5-flash', 'gemini-2.5-pro'],
85
- embedding: ['gemini-embedding-001'],
83
+ chat: ['gemini-3.5-flash-lite', 'gemini-3.5-flash'],
84
+ embedding: ['gemini-embedding-2'],
86
85
  },
87
86
  vertex: {
88
- chat: ['gemini-2.5-flash', 'gemini-2.5-pro'],
89
- embedding: ['text-embedding-005', 'gemini-embedding-001'],
87
+ chat: ['gemini-3.5-flash-lite', 'gemini-3.5-flash'],
88
+ embedding: ['gemini-embedding-2'],
90
89
  },
91
90
  openrouter: {
92
- chat: ['openai/gpt-4o-mini', 'anthropic/claude-haiku-4.5'],
91
+ chat: ['openai/gpt-5.6-luna', 'anthropic/claude-sonnet-5'],
93
92
  embedding: ['openai/text-embedding-3-small'],
94
93
  },
95
94
  };
package/dist/command.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { BannerText } from './banner.ts';
1
2
  export interface Context {
2
3
  readonly args: readonly string[];
3
4
  readonly json: boolean;
@@ -9,6 +10,8 @@ export interface Command {
9
10
  readonly usage: string;
10
11
  /** lines printed under the usage line by `zen help <command>` */
11
12
  readonly details?: readonly string[];
13
+ /** printed instead of the `zen` banner, so the sub-brand shows through */
14
+ readonly banner?: BannerText;
12
15
  /**
13
16
  * Answers `--help` itself, for a command whose arguments select a page the
14
17
  * frame cannot see. Without it the frame prints this command's own page.
@@ -1,6 +1,7 @@
1
1
  import { existsSync, statSync } from 'node:fs';
2
2
  import { basename, dirname, resolve } from 'node:path';
3
3
  import { one, parse } from "../args.js";
4
+ import { loadProjectEnv } from "../env.js";
4
5
  import { KeyStore } from "../keys.js";
5
6
  import { duration } from "../narrate.js";
6
7
  import { Registry } from "../projects.js";
@@ -75,7 +76,10 @@ export const check = {
75
76
  const name = entry?.name ?? basename(dir);
76
77
  // Materialised first, so the credential verdicts are the ones a run
77
78
  // would reach: a key in the environment and a key in the keyring are
78
- // the same key by the time the library asks.
79
+ // the same key by the time the library asks. The project's own `.env`
80
+ // goes first for the same reason — `zen run` reads it, so a check that
81
+ // did not would report a missing credential for a project that runs.
82
+ loadProjectEnv(dir);
79
83
  const keys = await KeyStore.open();
80
84
  keys.materialize();
81
85
  const bar = progress();
@@ -5,6 +5,7 @@ import { inspect } from "./inspect.js";
5
5
  import { key } from "./key.js";
6
6
  import { list } from "./list.js";
7
7
  import { memory } from "./memory.js";
8
+ import { meta } from "./meta.js";
8
9
  import { models } from "./models.js";
9
10
  import { open } from "./open.js";
10
11
  import { run } from "./run.js";
@@ -19,6 +20,7 @@ export const COMMANDS = {
19
20
  init,
20
21
  list,
21
22
  run,
23
+ meta,
22
24
  open,
23
25
  key,
24
26
  models,
@@ -37,6 +39,7 @@ export const ALIASES = {
37
39
  model: 'models',
38
40
  validate: 'check',
39
41
  doctor: 'check',
42
+ copilot: 'meta',
40
43
  report: 'inspect',
41
44
  mem: 'memory',
42
45
  edit: 'open',
@@ -49,7 +52,7 @@ export const EXTERNAL = {
49
52
  summary: 'A mock API from an openapi/swagger document.',
50
53
  usage: 'zen faker <serve|build|cache> [spec...]',
51
54
  install: 'npm i -g @zenera/faker',
52
- banner: { head: 'Zenera', accent: 'Faker', subtitle: 'Mock API Server' },
55
+ banner: { head: 'Zenera', accent: 'Faker', subtitle: 'Mock API Server', hue: 'mint' },
53
56
  },
54
57
  rag: {
55
58
  package: '@zenera/rag',
@@ -2,6 +2,7 @@ import { readProjectConfig } from '@zenera/neo';
2
2
  import { existsSync, readdirSync } from 'node:fs';
3
3
  import { basename, resolve } from 'node:path';
4
4
  import { one, parse } from "../args.js";
5
+ import { PREFERRED } from "../catalog.js";
5
6
  import { ensureHome } from "../home.js";
6
7
  import { resolveBuild } from "../image.js";
7
8
  import { envNames, isProvider, keyId, KeyStore, PROVIDERS } from "../keys.js";
@@ -43,6 +44,36 @@ const DEFAULT_MODEL = {
43
44
  vertex: { ref: 'vertex:gemini-3.5-flash' },
44
45
  openrouter: { ref: 'openrouter:inclusionai/ling-3.0-flash-fin:free' },
45
46
  };
47
+ /**
48
+ * What vectorises the project's memory, given the provider it ends up on.
49
+ *
50
+ * Taken from the same table `zen models pick` ranks by rather than written out
51
+ * again here, so the scaffold cannot drift from what the catalogue calls
52
+ * current. Anthropic publishes no embeddings API, and a `--model` naming a
53
+ * provider we do not ship gives nothing to guess from; both scaffold without
54
+ * one, and memory still works by ranking recall on term overlap. An
55
+ * `embeddings:` entry pointing at a provider that has none is not a degraded
56
+ * project, it is one that fails to load.
57
+ */
58
+ function defaultEmbedding(provider) {
59
+ if (provider === undefined) {
60
+ return undefined;
61
+ }
62
+ const id = PREFERRED[provider].embedding[0];
63
+ return id === undefined ? undefined : `${provider}:${id}`;
64
+ }
65
+ /**
66
+ * The provider a model ref names, when it names one we know.
67
+ *
68
+ * Every ref the scaffold writes carries its provider, so this is how the
69
+ * memory store ends up on the same credential as the chat model — including
70
+ * for an explicit `--model`, where nothing else says which vendor was meant.
71
+ */
72
+ function providerOf(ref) {
73
+ const colon = ref.indexOf(':');
74
+ const head = colon < 0 ? '' : ref.slice(0, colon);
75
+ return isProvider(head) ? head : undefined;
76
+ }
46
77
  /**
47
78
  * The provider this machine can actually reach.
48
79
  *
@@ -96,10 +127,11 @@ export const init = {
96
127
  summary: 'Create a project here, or in <dir>, and register it.',
97
128
  usage: USAGE,
98
129
  details: [
99
- 'Writes SPECIFICATION.md, INSTRUCTIONS.md, agents.yaml, agents/ and',
100
- 'scripts/, then records the directory so `zen list` and `zen open` can',
101
- 'find it by name. Editor files (.vscode/settings.json and the .github/',
102
- 'tree) are written alongside, replacing any already there.',
130
+ 'Writes SPECIFICATION.md, agents.yaml, agents/ (house rules, the memory',
131
+ 'rules and one role prompt) and scripts/, then records the directory so',
132
+ '`zen list` and `zen open` can find it by name. Editor files',
133
+ '(.vscode/settings.json and the .github/ tree) are written alongside,',
134
+ 'replacing any already there.',
103
135
  '',
104
136
  'A directory that is already a project is adopted rather than rebuilt:',
105
137
  'it is registered under its own name, and nothing in it is touched.',
@@ -108,10 +140,12 @@ export const init = {
108
140
  '',
109
141
  'SPECIFICATION.md states what the project is for and what it may do,',
110
142
  'and describes the one that was just written. Say what you are building',
111
- 'there, then run /sync-with-spec in your editor to make the rest match.',
143
+ 'there, then run /spec-sync-project in your editor to make the rest match.',
112
144
  '',
113
145
  'The default agent gets the file tools and a sandboxed shell, plus',
114
- '`exa:*` when the keyring holds an Exa key. Without --model, the',
146
+ '`exa:*` when the keyring holds an Exa key. It keeps a memory of its',
147
+ 'own work in memory/, vectorised by the same provider as the model',
148
+ 'when that provider publishes embeddings. Without --model, the',
115
149
  'keyring is checked and the model is picked from a credential the',
116
150
  'provider accepts.',
117
151
  '',
@@ -143,7 +177,8 @@ export const init = {
143
177
  : DEFAULT_MODEL[provider ?? 'openai'];
144
178
  const model = choice.ref;
145
179
  const web = hasExa(store);
146
- const written = scaffold({ dir, model, modelOptions: choice.options, web });
180
+ const embedding = defaultEmbedding(providerOf(model));
181
+ const written = scaffold({ dir, model, modelOptions: choice.options, web, embedding });
147
182
  const registry = await Registry.open();
148
183
  registry.add(name, dir);
149
184
  registry.save();