@zenera/cli 1.1.23 → 1.1.25

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 (184) hide show
  1. package/README.md +88 -2
  2. package/dist/ask.d.ts +51 -0
  3. package/dist/ask.js +166 -0
  4. package/dist/banner.js +2 -2
  5. package/dist/batch.d.ts +83 -0
  6. package/dist/batch.js +413 -0
  7. package/dist/catalog.d.ts +11 -1
  8. package/dist/catalog.js +80 -7
  9. package/dist/command.d.ts +6 -0
  10. package/dist/commands/inspect.d.ts +57 -0
  11. package/dist/commands/inspect.js +539 -91
  12. package/dist/commands/list.js +128 -19
  13. package/dist/commands/memory.js +461 -32
  14. package/dist/commands/meta.js +311 -21
  15. package/dist/commands/models.js +110 -14
  16. package/dist/commands/open.js +4 -4
  17. package/dist/commands/run.js +175 -36
  18. package/dist/commands/sandbox.js +148 -15
  19. package/dist/engine.d.ts +24 -0
  20. package/dist/engine.js +61 -8
  21. package/dist/eventlog.d.ts +59 -0
  22. package/dist/eventlog.js +146 -0
  23. package/dist/liveness.d.ts +7 -0
  24. package/dist/liveness.js +20 -3
  25. package/dist/main.js +5 -2
  26. package/dist/meta/editor.d.ts +9 -0
  27. package/dist/meta/editor.js +60 -0
  28. package/dist/meta/finetune/children.d.ts +8 -0
  29. package/dist/meta/finetune/children.js +76 -0
  30. package/dist/meta/finetune/command.d.ts +30 -0
  31. package/dist/meta/finetune/command.js +338 -0
  32. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  33. package/dist/meta/finetune/dataset/anchors.js +239 -0
  34. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  35. package/dist/meta/finetune/dataset/changes.js +319 -0
  36. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  37. package/dist/meta/finetune/dataset/command.js +1011 -0
  38. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  39. package/dist/meta/finetune/dataset/drift.js +71 -0
  40. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  41. package/dist/meta/finetune/dataset/export.js +51 -0
  42. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  43. package/dist/meta/finetune/dataset/index.js +2 -0
  44. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  45. package/dist/meta/finetune/dataset/sample.js +197 -0
  46. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  47. package/dist/meta/finetune/dataset/schema.js +216 -0
  48. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  49. package/dist/meta/finetune/dataset/store.js +177 -0
  50. package/dist/meta/finetune/dataset/types.d.ts +125 -0
  51. package/dist/meta/finetune/dataset/types.js +39 -0
  52. package/dist/meta/finetune/failure.d.ts +15 -0
  53. package/dist/meta/finetune/failure.js +26 -0
  54. package/dist/meta/finetune/feedback.d.ts +45 -0
  55. package/dist/meta/finetune/feedback.js +85 -0
  56. package/dist/meta/finetune/improvements.d.ts +51 -0
  57. package/dist/meta/finetune/improvements.js +244 -0
  58. package/dist/meta/finetune/index.d.ts +2 -0
  59. package/dist/meta/finetune/index.js +2 -0
  60. package/dist/meta/finetune/journal.d.ts +35 -0
  61. package/dist/meta/finetune/journal.js +135 -0
  62. package/dist/meta/finetune/live.d.ts +74 -0
  63. package/dist/meta/finetune/live.js +226 -0
  64. package/dist/meta/finetune/loop.d.ts +3 -0
  65. package/dist/meta/finetune/loop.js +114 -0
  66. package/dist/meta/finetune/memories.d.ts +47 -0
  67. package/dist/meta/finetune/memories.js +154 -0
  68. package/dist/meta/finetune/report.d.ts +33 -0
  69. package/dist/meta/finetune/report.js +1284 -0
  70. package/dist/meta/finetune/retry.d.ts +24 -0
  71. package/dist/meta/finetune/retry.js +96 -0
  72. package/dist/meta/finetune/sampler.d.ts +18 -0
  73. package/dist/meta/finetune/sampler.js +41 -0
  74. package/dist/meta/finetune/system.d.ts +27 -0
  75. package/dist/meta/finetune/system.js +132 -0
  76. package/dist/meta/finetune/train.d.ts +39 -0
  77. package/dist/meta/finetune/train.js +391 -0
  78. package/dist/meta/finetune/tui.d.ts +16 -0
  79. package/dist/meta/finetune/tui.js +291 -0
  80. package/dist/meta/finetune/tuning.d.ts +167 -0
  81. package/dist/meta/finetune/tuning.js +289 -0
  82. package/dist/meta/finetune/usage.d.ts +62 -0
  83. package/dist/meta/finetune/usage.js +225 -0
  84. package/dist/meta/host.d.ts +14 -0
  85. package/dist/meta/host.js +20 -0
  86. package/dist/meta/index.d.ts +11 -0
  87. package/dist/meta/index.js +11 -0
  88. package/dist/meta/launch.d.ts +81 -0
  89. package/dist/meta/launch.js +286 -0
  90. package/dist/meta/model.d.ts +44 -0
  91. package/dist/meta/model.js +119 -0
  92. package/dist/meta/prompts.d.ts +18 -0
  93. package/dist/meta/prompts.js +61 -0
  94. package/dist/meta/relay.d.ts +20 -0
  95. package/dist/meta/relay.js +123 -0
  96. package/dist/meta/tokens.d.ts +52 -0
  97. package/dist/meta/tokens.js +226 -0
  98. package/dist/meta/window.d.ts +61 -0
  99. package/dist/meta/window.js +198 -0
  100. package/dist/meta/wire.d.ts +42 -0
  101. package/dist/meta/wire.js +129 -0
  102. package/dist/modelref.d.ts +16 -0
  103. package/dist/modelref.js +41 -0
  104. package/dist/narrate.js +5 -2
  105. package/dist/podman.d.ts +42 -0
  106. package/dist/podman.js +198 -5
  107. package/dist/progress.d.ts +33 -0
  108. package/dist/progress.js +402 -0
  109. package/dist/projects.d.ts +0 -5
  110. package/dist/projects.js +2 -15
  111. package/dist/request.d.ts +45 -0
  112. package/dist/request.js +300 -0
  113. package/dist/sandbox.d.ts +2 -0
  114. package/dist/sandbox.js +20 -3
  115. package/dist/scaffold.d.ts +15 -6
  116. package/dist/scaffold.js +5 -60
  117. package/dist/session.d.ts +26 -1
  118. package/dist/session.js +46 -32
  119. package/dist/term.d.ts +5 -0
  120. package/dist/term.js +28 -3
  121. package/dist/trace.d.ts +84 -0
  122. package/dist/trace.js +737 -0
  123. package/dist/tui/models.d.ts +33 -0
  124. package/dist/tui/models.js +389 -0
  125. package/dist/tui/wrap.d.ts +5 -0
  126. package/dist/tui/wrap.js +12 -0
  127. package/dist/usage.d.ts +78 -0
  128. package/dist/usage.js +51 -0
  129. package/dist/validate.js +25 -3
  130. package/package.json +3 -2
  131. package/templates/editor/.github/copilot-instructions.md +41 -107
  132. package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
  133. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  134. package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
  135. package/templates/editor/.github/prompts/finetune.prompt.md +744 -0
  136. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  137. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  138. package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
  139. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
  140. package/templates/editor/.github/skills/zen-cli/SKILL.md +18 -4
  141. package/templates/editor/.github/skills/zen-cli/references/check.md +7 -5
  142. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  143. package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
  144. package/templates/editor/.github/skills/zen-cli/references/inspect.md +300 -21
  145. package/templates/editor/.github/skills/zen-cli/references/memory.md +121 -28
  146. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  147. package/templates/editor/.github/skills/zen-cli/references/models.md +6 -2
  148. package/templates/editor/.github/skills/zen-cli/references/projects.md +12 -5
  149. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  150. package/templates/editor/.github/skills/zen-cli/references/run.md +201 -4
  151. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
  152. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  153. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  154. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  155. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1791 -0
  156. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  157. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  158. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  159. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  160. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  161. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  162. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  163. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  164. package/templates/editor/.github/skills/zen-inspect/SKILL.md +431 -0
  165. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
  166. package/templates/editor/.github/skills/zen-memory/SKILL.md +78 -29
  167. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +363 -531
  168. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  169. package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
  170. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
  171. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +25 -2
  172. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +334 -0
  173. package/templates/editor/.github/skills/zen-sandbox-capacity/references/loadtest.md +181 -0
  174. package/templates/editor/.github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh +457 -0
  175. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  176. package/templates/editor/.vscode/agents.schema.json +1 -1
  177. package/templates/editor/.vscode/settings.json +2 -0
  178. package/templates/project/agents/files-instructions.md +35 -13
  179. package/templates/project/agents/fork-instructions.md +27 -0
  180. package/templates/project/agents/memory-instructions.md +106 -34
  181. package/templates/project/agents/tools-instructions.md +7 -4
  182. package/templates/project/gitignore +5 -0
  183. package/dist/meta.d.ts +0 -221
  184. package/dist/meta.js +0 -684
package/dist/batch.js ADDED
@@ -0,0 +1,413 @@
1
+ import { addUsage, lockHolder, MANIFEST_FILE as MEMORY_MANIFEST, memoryDir, MemoryStore, mergeModelUsage, readProjectConfig, SANDBOX_GROUP, selectTools, usageByModel, zeroUsage, } from '@zenera/neo';
2
+ import { existsSync } from 'node:fs';
3
+ import { cp, mkdir, rm, writeFile } from 'node:fs/promises';
4
+ import { basename, join, resolve } from 'node:path';
5
+ import * as Engine from "./engine.js";
6
+ import { stamp } from "./ids.js";
7
+ import { duration } from "./narrate.js";
8
+ import { BatchProgress } from "./progress.js";
9
+ import { assertLocks } from "./podman.js";
10
+ import { project as resolveProject, target } from "./resolve.js";
11
+ import { display } from "./session.js";
12
+ import { CliError, count, cyan, dim, EXIT, invalidError, json, jsonText, note, usageError, write, } from "./term.js";
13
+ import { appendUsage } from "./usage.js";
14
+ import { availableTools } from "./validate.js";
15
+ /** Past this, the limit is the provider's rate limit and the failures are 429s. */
16
+ const MAX_CONCURRENCY = 32;
17
+ export async function runBatch(opts) {
18
+ const project = await resolveProject({ cwd: opts.cwd, project: opts.project });
19
+ const items = opts.request.items;
20
+ // Every item at once holds its own containers, so a short engine fails
21
+ // here once instead of once per item.
22
+ const boxes = containersPerItem(project.dir);
23
+ if (boxes > 0) {
24
+ await assertLocks(boxes * Math.min(opts.concurrency, items.length));
25
+ }
26
+ const dir = opts.dir ? resolve(opts.cwd, opts.dir) : join(project.dir, 'batches', stamp());
27
+ // Two batches in one directory would interleave their item folders and the
28
+ // second would overwrite the first's index while the first still points at
29
+ // it. Cheaper to refuse than to explain.
30
+ if (existsSync(join(dir, 'batch.json'))) {
31
+ throw invalidError(`${dir} already holds a batch`, 'name an empty directory with --batch-dir, or leave it out for a fresh one');
32
+ }
33
+ await mkdir(dir, { recursive: true });
34
+ const source = memorySource(project.dir, opts.memory);
35
+ const mode = source ? (opts.memoryReadOnly ? 'read-only' : 'copied') : 'none';
36
+ // A project declares its memory directory; it is the first run that makes
37
+ // one. So a batch may legitimately be pointed at a graph that is not there
38
+ // yet - which is the whole of warming a cold project.
39
+ const seeded = source !== undefined && existsSync(source);
40
+ if (opts.memoryReadOnly && !source) {
41
+ throw usageError('--memory-read-only, but this project has no memory', 'give one with --memory, or drop the flag');
42
+ }
43
+ if (source && seeded) {
44
+ // A graph is three files that only mean anything together. Reading one
45
+ // mid-commit is a wrong answer and copying one is a wrong archive, so
46
+ // both modes wait rather than race.
47
+ const held = lockHolder(source);
48
+ if (held) {
49
+ throw invalidError(`that memory is in use (pid ${held.pid}, since ${held.startedAt})`, 'wait for that run to finish, or point --memory at another directory');
50
+ }
51
+ }
52
+ // Serially, and before anything runs: sixteen recursive copies of the same
53
+ // tree at once is the one moment a batch is disk-bound, and it would be
54
+ // spent racing itself.
55
+ const prepared = await prepareMemories(items, { dir, mode, source, seeded });
56
+ // Opened once, here, so a path that is not a memory is one error rather
57
+ // than one per item — and so the read-only refusal lands before any cost.
58
+ if (mode === 'read-only' && source) {
59
+ await MemoryStore.open(source, { readOnly: true });
60
+ }
61
+ // Ctrl-C asks every run to stop rather than killing the process: each one
62
+ // still lands its turn, and each finished item keeps the answer it already
63
+ // wrote.
64
+ const stopping = new AbortController();
65
+ const onInterrupt = () => stopping.abort();
66
+ process.once('SIGINT', onInterrupt);
67
+ if (!opts.json) {
68
+ note(`${cyan(project.name)} ${dim(`${count(items.length, 'item')}, ${opts.concurrency} at a time`)}`);
69
+ note(dim(`memory: ${source ? `${mode}${seeded ? '' : ' (new)'}, ${display(source, opts.cwd)}` : 'none'}`));
70
+ note(dim(`batch: ${display(dir, opts.cwd)}`));
71
+ note(dim(`watch: ${display(join(dir, 'README.md'), opts.cwd)}`));
72
+ note('');
73
+ }
74
+ // The dashboard, from here until the last item lands.
75
+ const progress = new BatchProgress({
76
+ dir,
77
+ project: project.name,
78
+ projectDir: project.dir,
79
+ input: opts.input,
80
+ concurrency: opts.concurrency,
81
+ memory: source ? `${mode}${seeded ? '' : ' (new)'} — \`${source}\`` : 'none',
82
+ items,
83
+ });
84
+ progress.begin();
85
+ const startedAt = new Date();
86
+ let done = 0;
87
+ // What each item spent, per model. Collected here rather than read back
88
+ // out of the dashboard, which is deliberately not load-bearing.
89
+ const spent = [];
90
+ const results = await pool(items, opts.concurrency, async (item) => {
91
+ const result = await runItem(item, {
92
+ opts,
93
+ projectDir: project.dir,
94
+ dir,
95
+ memory: prepared.get(item.id),
96
+ memoryReadOnly: mode === 'read-only',
97
+ signal: stopping.signal,
98
+ progress,
99
+ spent,
100
+ });
101
+ done += 1;
102
+ if (!opts.json) {
103
+ note(`${dim(`[${done}/${items.length}]`)} ` +
104
+ `${result.ok ? cyan('ok') : 'failed'} ${item.id}` +
105
+ `${result.error ? dim(` — ${result.error.message}`) : ''}`);
106
+ }
107
+ return result;
108
+ });
109
+ process.off('SIGINT', onInterrupt);
110
+ await progress.close();
111
+ // An item that was given an empty graph and found nothing worth keeping
112
+ // leaves a directory that is not a memory, and `merge` rightly refuses
113
+ // one. Nothing was learned there, so there is nothing to leave behind.
114
+ const written = mode === 'copied' ? await keepWritten(prepared) : 0;
115
+ const finishedAt = new Date();
116
+ const failed = results.filter((r) => !r.ok).length;
117
+ const models = mergeModelUsage(...spent);
118
+ const body = {
119
+ batch: {
120
+ dir,
121
+ readme: join(dir, 'README.md'),
122
+ input: opts.input,
123
+ project: project.dir,
124
+ items: items.length,
125
+ ok: results.length - failed,
126
+ failed,
127
+ concurrency: opts.concurrency,
128
+ memory: { source: source ?? null, mode },
129
+ usage: models.reduce((sum, m) => addUsage(sum, m.usage), zeroUsage()),
130
+ models,
131
+ startedAt: startedAt.toISOString(),
132
+ finishedAt: finishedAt.toISOString(),
133
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
134
+ },
135
+ batch_results: results,
136
+ };
137
+ const text = jsonText(body);
138
+ await writeFile(join(dir, 'batch.json'), text, 'utf8');
139
+ appendUsage(project.dir, {
140
+ kind: 'batch',
141
+ ts: finishedAt.toISOString(),
142
+ batchDir: dir,
143
+ input: opts.input,
144
+ items: items.length,
145
+ ok: results.length - failed,
146
+ failed,
147
+ concurrency: opts.concurrency,
148
+ memory: mode,
149
+ startedAt: startedAt.toISOString(),
150
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
151
+ models,
152
+ });
153
+ if (opts.out) {
154
+ await writeFile(resolve(opts.cwd, opts.out), text, 'utf8');
155
+ }
156
+ if (opts.json) {
157
+ json(body);
158
+ }
159
+ else {
160
+ note('');
161
+ note(`${count(results.length - failed, 'run')} ok, ${failed} failed ` +
162
+ dim(duration(finishedAt.getTime() - startedAt.getTime())));
163
+ if (mode === 'copied' && written > 0) {
164
+ note(dim(`fold what they learned back in: zen memory merge ${dir}/*/memory`));
165
+ }
166
+ // The batch directory is the answer, and it is the whole answer: every
167
+ // path in the index is under it.
168
+ write(dir);
169
+ }
170
+ // After everything is written. A batch that ends in a non-zero exit still
171
+ // has to leave behind the answers it did get.
172
+ if (failed > 0) {
173
+ throw new CliError(`${count(failed, 'item')} failed`, EXIT.failed, `what each one said: ${join(dir, 'batch.json')}`);
174
+ }
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // One item
178
+ // ---------------------------------------------------------------------------
179
+ /**
180
+ * The most containers one item can start: one shared by the agents on the
181
+ * project's sandbox block, one more for each agent with a block of its own.
182
+ * A project that does not load is each item's to report, so it counts none.
183
+ */
184
+ export function containersPerItem(dir) {
185
+ let read;
186
+ try {
187
+ read = readProjectConfig(dir);
188
+ }
189
+ catch {
190
+ return 0;
191
+ }
192
+ const tools = availableTools(read.root, read.config);
193
+ const shells = read.config.agents.filter((a) => {
194
+ try {
195
+ return selectTools(tools, [...(a.tools ?? [])], { where: a.name }).some((t) => t.group === SANDBOX_GROUP);
196
+ }
197
+ catch {
198
+ return false;
199
+ }
200
+ });
201
+ const own = shells.filter((a) => a.sandbox).length;
202
+ return own + (shells.length > own ? 1 : 0);
203
+ }
204
+ /**
205
+ * A failure is data. One item that asks something the model cannot answer must
206
+ * not cost the other ninety-nine their answers, so everything is caught and
207
+ * written down, and the exit code is decided once at the end.
208
+ */
209
+ async function runItem(item, ctx) {
210
+ const at = join(ctx.dir, item.id);
211
+ const output = join(at, 'output.json');
212
+ const watch = ctx.progress.watch(item.id);
213
+ try {
214
+ const where = await target({
215
+ cwd: ctx.opts.cwd,
216
+ project: ctx.projectDir,
217
+ // Every item is its own conversation. There is no session to
218
+ // resume and nobody to ask which one.
219
+ fresh: true,
220
+ workspace: item.workspace ?? join(at, 'workspace'),
221
+ yes: true,
222
+ });
223
+ const engine = await Engine.open({
224
+ project: where.project,
225
+ session: where.session,
226
+ readOnly: ctx.opts.readOnly,
227
+ model: ctx.opts.model,
228
+ image: ctx.opts.image,
229
+ memoryDir: ctx.memory,
230
+ memoryReadOnly: ctx.memoryReadOnly,
231
+ keys: ctx.opts.keys,
232
+ yes: true,
233
+ // The project's own problems are the batch's, not each item's.
234
+ quiet: true,
235
+ // A fresh session nobody resumes: a stopped container per item
236
+ // is how a few fine-tuning rounds used up every engine lock.
237
+ disposable: true,
238
+ });
239
+ try {
240
+ // Nothing is narrated to the terminal — a hundred interleaved
241
+ // streams there is not progress, it is noise. The events go to the
242
+ // dashboard instead, where each item has a line of its own.
243
+ const outcome = await Engine.run(engine, item.input, watch, ctx.signal);
244
+ // Written the moment it is known, not at the end — a batch killed
245
+ // halfway still has every answer it managed to get.
246
+ await writeFile(output, jsonText(Engine.envelope(engine, outcome)), 'utf8');
247
+ const models = usageByModel(outcome.result.state.trajectory);
248
+ ctx.spent.push(models);
249
+ appendUsage(ctx.projectDir, {
250
+ kind: 'run',
251
+ ts: new Date().toISOString(),
252
+ runDir: outcome.run.dir,
253
+ session: engine.session.id,
254
+ batchDir: ctx.dir,
255
+ item: item.id,
256
+ ok: true,
257
+ stopReason: outcome.result.stopReason,
258
+ durationMs: outcome.durationMs,
259
+ models,
260
+ });
261
+ ctx.progress.finish(item.id, { ok: true, usage: outcome.result.usage, models });
262
+ return { index: item.index, id: item.id, ok: true, input: index(item.input), output };
263
+ }
264
+ finally {
265
+ await engine.close();
266
+ }
267
+ }
268
+ catch (err) {
269
+ const error = {
270
+ message: err instanceof Error ? err.message : String(err),
271
+ ...(err instanceof CliError && err.hint ? { hint: err.hint } : {}),
272
+ };
273
+ await writeFile(output, jsonText({ ok: false, id: item.id, error }), 'utf8');
274
+ appendUsage(ctx.projectDir, {
275
+ kind: 'run',
276
+ ts: new Date().toISOString(),
277
+ batchDir: ctx.dir,
278
+ item: item.id,
279
+ ok: false,
280
+ error: error.message,
281
+ models: [],
282
+ });
283
+ ctx.progress.finish(item.id, { ok: false, error: error.message });
284
+ return {
285
+ index: item.index,
286
+ id: item.id,
287
+ ok: false,
288
+ input: index(item.input),
289
+ output,
290
+ error,
291
+ };
292
+ }
293
+ }
294
+ // ---------------------------------------------------------------------------
295
+ // Parts
296
+ // ---------------------------------------------------------------------------
297
+ /**
298
+ * A bounded map that keeps its order. Workers pull from one cursor rather than
299
+ * taking a slice each, so a batch of one slow item and fifty fast ones finishes
300
+ * in the time of the slow one instead of the time of the slowest slice.
301
+ *
302
+ * `worker` is expected not to throw; `runItem` is the only caller and catches.
303
+ */
304
+ export async function pool(items, limit, worker) {
305
+ const out = new Array(items.length);
306
+ let next = 0;
307
+ const runner = async () => {
308
+ for (;;) {
309
+ const i = next;
310
+ next += 1;
311
+ if (i >= items.length) {
312
+ return;
313
+ }
314
+ out[i] = await worker(items[i], i);
315
+ }
316
+ };
317
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runner));
318
+ return out;
319
+ }
320
+ /** `--concurrency`, or the default. */
321
+ export function concurrency(value) {
322
+ if (value === undefined) {
323
+ return 16;
324
+ }
325
+ const n = Number(value);
326
+ if (!Number.isInteger(n) || n < 1) {
327
+ throw usageError(`--concurrency ${value}: expected a whole number, 1 or more`);
328
+ }
329
+ if (n > MAX_CONCURRENCY) {
330
+ throw usageError(`--concurrency ${n} is more than this runs at once`, `${MAX_CONCURRENCY} is the most; past that the limit is the provider's, not ours`);
331
+ }
332
+ return n;
333
+ }
334
+ /**
335
+ * Each item's memory, made before any of them runs.
336
+ *
337
+ * In the copying mode every item gets its own graph to write into, so that
338
+ * sixteen runs at once are sixteen private histories rather than one contended
339
+ * lock. A source that is not there yet is not an error: a project declares its
340
+ * memory directory and the first run makes it, so warming a cold project means
341
+ * copying from nothing, and nothing is what each item starts with.
342
+ */
343
+ export async function prepareMemories(items, at) {
344
+ const prepared = new Map();
345
+ for (const item of items) {
346
+ await mkdir(join(at.dir, item.id), { recursive: true });
347
+ if (at.mode === 'copied' && at.source) {
348
+ const mine = join(at.dir, item.id, 'memory');
349
+ if (at.seeded) {
350
+ await cp(at.source, mine, { recursive: true, filter: notLock });
351
+ }
352
+ prepared.set(item.id, mine);
353
+ }
354
+ else if (at.mode === 'read-only' && at.source) {
355
+ prepared.set(item.id, at.source);
356
+ }
357
+ }
358
+ return prepared;
359
+ }
360
+ /**
361
+ * Drops the memories nothing was committed to, and counts the ones left.
362
+ *
363
+ * A memory is its manifest and the two files beside it; a directory without
364
+ * one is not a memory and `merge` refuses it by name. Leaving those behind
365
+ * would mean the `merge` line this command prints is advice that fails, so an
366
+ * item that learned nothing leaves nothing.
367
+ */
368
+ export async function keepWritten(prepared) {
369
+ let kept = 0;
370
+ for (const at of prepared.values()) {
371
+ if (existsSync(join(at, MEMORY_MANIFEST))) {
372
+ kept += 1;
373
+ }
374
+ else {
375
+ await rm(at, { recursive: true, force: true });
376
+ }
377
+ }
378
+ return kept;
379
+ }
380
+ /** Where the batch reads memory from: `--memory`, else what the project says. */
381
+ function memorySource(projectDir, override) {
382
+ try {
383
+ return memoryDir(projectDir, readProjectConfig(projectDir).config, override);
384
+ }
385
+ catch {
386
+ // An unloadable project is about to fail properly, with its own error.
387
+ return override;
388
+ }
389
+ }
390
+ /** The lock belongs to the run that took it, never to a copy of its memory. */
391
+ const notLock = (src) => basename(src) !== '.lock';
392
+ /**
393
+ * The question, for the index — text whole, media named rather than repeated.
394
+ * Every byte is already in the item's own run input file, and a combined file
395
+ * carrying forty inlined screenshots is not an index of anything.
396
+ */
397
+ function index(input) {
398
+ if (typeof input === 'string') {
399
+ return input;
400
+ }
401
+ const parts = Array.isArray(input) ? input : [input];
402
+ return parts.map((part) => summarise(part));
403
+ }
404
+ function summarise(part) {
405
+ if (part.type === 'text') {
406
+ return part;
407
+ }
408
+ if (part.url.startsWith('data:')) {
409
+ return { type: part.type, mimeType: part.mimeType, bytes: part.url.length, inlined: true };
410
+ }
411
+ return part;
412
+ }
413
+ //# sourceMappingURL=batch.js.map
package/dist/catalog.d.ts CHANGED
@@ -36,6 +36,8 @@ export interface CatalogEntry {
36
36
  };
37
37
  /** ISO date the model was published, when known */
38
38
  created?: string;
39
+ /** vertex only: who makes it - `google`, or the partner Vertex resells */
40
+ publisher?: string;
39
41
  source: 'live' | 'curated';
40
42
  }
41
43
  /** A provider's listing, and an honest account of where it came from. */
@@ -72,6 +74,7 @@ export declare const PREFERRED: Record<Provider, {
72
74
  chat: readonly string[];
73
75
  embedding: readonly string[];
74
76
  }>;
77
+ export declare function fromVertexPartners(): Promise<CatalogEntry[]>;
75
78
  /**
76
79
  * Asks one provider what it serves. Throws whatever the SDK throws.
77
80
  *
@@ -79,8 +82,9 @@ export declare const PREFERRED: Record<Provider, {
79
82
  * to break when a vendor reshapes a payload, and they are untestable if the
80
83
  * only way to reach them is a credential and a network. It mirrors
81
84
  * `ProviderSpec.client`, which exists in the library for the same reason.
85
+ * `partners` is the same seam for Vertex's second listing.
82
86
  */
83
- export declare function fetchCatalog(provider: Provider, client?: unknown): Promise<CatalogEntry[]>;
87
+ export declare function fetchCatalog(provider: Provider, client?: unknown, partners?: () => Promise<CatalogEntry[]>): Promise<CatalogEntry[]>;
84
88
  /** The key one provider's listing is filed under, version and all. */
85
89
  export declare const catalogKey: (provider: Provider) => string;
86
90
  export interface CatalogOptions {
@@ -111,4 +115,10 @@ export interface Filters {
111
115
  export declare function matches(row: CatalogEntry, query: string, filters?: Filters): boolean;
112
116
  /** The providers a search covers when none was named. */
113
117
  export declare const catalogProviders: () => readonly Provider[];
118
+ /** Resold on Vertex rather than made by Google. */
119
+ export declare const isPartner: (e: CatalogEntry) => boolean;
120
+ /** Provider, then Google's own models, then each partner's together. */
121
+ export declare function byGroup(a: CatalogEntry, b: CatalogEntry): number;
122
+ /** The heading a row is listed under, when its provider resells other makers' models. */
123
+ export declare function groupOf(e: CatalogEntry): string | undefined;
114
124
  //# sourceMappingURL=catalog.d.ts.map
package/dist/catalog.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // `source` for exactly that reason: a curated guess must never be mistaken for
17
17
  // the vendor's own word.
18
18
  // ---------------------------------------------------------------------------
19
- import { ModelRegistry } from '@zenera/neo';
19
+ import { ModelRegistry, vertexEndpoint } from '@zenera/neo';
20
20
  import { Cache, cacheKey } from "./cache.js";
21
21
  import { PROVIDERS } from "./keys.js";
22
22
  import { classify } from "./liveness.js";
@@ -24,7 +24,7 @@ import { classify } from "./liveness.js";
24
24
  export const CATALOG_TTL_MS = 24 * 60 * 60 * 1000;
25
25
  export const CATALOG_KIND = 'catalog';
26
26
  /** Bumped when the stored shape changes, which makes every old entry a miss. */
27
- const CATALOG_VERSION = 1;
27
+ const CATALOG_VERSION = 2;
28
28
  // ---------------------------------------------------------------------------
29
29
  // The fallback table
30
30
  // ---------------------------------------------------------------------------
@@ -53,9 +53,17 @@ export const CURATED = {
53
53
  { id: 'gemini-embedding-2', roles: ['embedding'] },
54
54
  ],
55
55
  vertex: [
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'] },
56
+ {
57
+ id: 'gemini-3.5-flash-lite',
58
+ roles: ['chat'],
59
+ contextLength: 1_048_576,
60
+ publisher: 'google',
61
+ },
62
+ { id: 'gemini-3.8-flash', roles: ['chat'], contextLength: 1_048_576, publisher: 'google' },
63
+ { id: 'gemini-embedding-2', roles: ['embedding'], publisher: 'google' },
64
+ // Open-weight models are served but not listed by Model Garden, so these are the only record.
65
+ { id: 'qwen/qwen3-235b-a22b-instruct-2507-maas', roles: ['chat'], publisher: 'qwen' },
66
+ { id: 'openai/gpt-oss-120b-maas', roles: ['chat'], publisher: 'openai' },
59
67
  ],
60
68
  openrouter: [
61
69
  { id: 'openai/gpt-5.6-luna', roles: ['chat'], contextLength: 1_050_000 },
@@ -227,6 +235,7 @@ async function fromGenAI(client, provider) {
227
235
  description: model.description,
228
236
  contextLength: model.inputTokenLimit,
229
237
  maxOutputTokens: model.outputTokenLimit,
238
+ publisher: provider === 'vertex' ? 'google' : undefined,
230
239
  }));
231
240
  }
232
241
  return rows;
@@ -296,6 +305,38 @@ async function fromRouter(client, provider) {
296
305
  }
297
306
  return rows;
298
307
  }
308
+ /**
309
+ * Partner models on Vertex. The GenAI listing holds Google's alone, so each
310
+ * partner's Model Garden listing is asked as well - only for publishers whose
311
+ * models the runtime can call: Claude at `rawPredict`, the rest at the
312
+ * OpenAI-compatible endpoint as `publisher/model`.
313
+ */
314
+ const VERTEX_PARTNERS = ['anthropic', 'xai'];
315
+ export async function fromVertexPartners() {
316
+ const at = vertexEndpoint();
317
+ const token = await at.token();
318
+ const lists = await Promise.all(VERTEX_PARTNERS.map(async (publisher) => {
319
+ const res = await fetch(`https://aiplatform.googleapis.com/v1beta1/publishers/${publisher}/models?pageSize=200`, {
320
+ headers: {
321
+ Authorization: `Bearer ${token}`,
322
+ 'x-goog-user-project': at.project,
323
+ },
324
+ });
325
+ if (!res.ok) {
326
+ return [];
327
+ }
328
+ const body = (await res.json());
329
+ return (body.publisherModels ?? [])
330
+ .map((m) => bareId(m.name ?? ''))
331
+ .filter((id) => id && !/embed|ocr/.test(id))
332
+ .map((id) => entry('vertex', {
333
+ id: publisher === 'anthropic' ? id : `${publisher}/${id}`,
334
+ roles: ['chat'],
335
+ publisher,
336
+ }));
337
+ }));
338
+ return lists.flat();
339
+ }
299
340
  /**
300
341
  * Asks one provider what it serves. Throws whatever the SDK throws.
301
342
  *
@@ -303,16 +344,27 @@ async function fromRouter(client, provider) {
303
344
  * to break when a vendor reshapes a payload, and they are untestable if the
304
345
  * only way to reach them is a credential and a network. It mirrors
305
346
  * `ProviderSpec.client`, which exists in the library for the same reason.
347
+ * `partners` is the same seam for Vertex's second listing.
306
348
  */
307
- export async function fetchCatalog(provider, client = new ModelRegistry().client(provider)) {
349
+ export async function fetchCatalog(provider, client = new ModelRegistry().client(provider), partners = fromVertexPartners) {
308
350
  switch (provider) {
309
351
  case 'openai':
310
352
  return enrich(provider, await fromOpenAI(client, provider));
311
353
  case 'anthropic':
312
354
  return enrich(provider, await fromAnthropic(client, provider));
313
355
  case 'google':
314
- case 'vertex':
315
356
  return enrich(provider, await fromGenAI(client, provider));
357
+ case 'vertex': {
358
+ // A partner listing that fails must not cost the Gemini one.
359
+ const [google, others] = await Promise.all([
360
+ fromGenAI(client, provider),
361
+ partners().catch(() => []),
362
+ ]);
363
+ const live = [...google, ...others];
364
+ const seen = new Set(live.map((r) => r.id));
365
+ const unlisted = curated(provider).filter((r) => r.publisher !== 'google' && !seen.has(r.id));
366
+ return enrich(provider, [...live, ...unlisted]);
367
+ }
316
368
  case 'openrouter':
317
369
  return enrich(provider, await fromRouter(client, provider));
318
370
  }
@@ -459,4 +511,25 @@ export function matches(row, query, filters = {}) {
459
511
  }
460
512
  /** The providers a search covers when none was named. */
461
513
  export const catalogProviders = () => PROVIDERS;
514
+ // ---------------------------------------------------------------------------
515
+ // Grouping
516
+ // ---------------------------------------------------------------------------
517
+ /** Resold on Vertex rather than made by Google. */
518
+ export const isPartner = (e) => e.publisher !== undefined && e.publisher !== 'google';
519
+ /** Provider, then Google's own models, then each partner's together. */
520
+ export function byGroup(a, b) {
521
+ return (a.provider.localeCompare(b.provider) ||
522
+ Number(isPartner(a)) - Number(isPartner(b)) ||
523
+ (a.publisher ?? '').localeCompare(b.publisher ?? '') ||
524
+ a.id.localeCompare(b.id));
525
+ }
526
+ /** The heading a row is listed under, when its provider resells other makers' models. */
527
+ export function groupOf(e) {
528
+ if (!e.publisher) {
529
+ return undefined;
530
+ }
531
+ return isPartner(e)
532
+ ? `${e.provider} · partner · ${e.publisher}`
533
+ : `${e.provider} · Google (Gemini, Gemma, Imagen, Veo)`;
534
+ }
462
535
  //# sourceMappingURL=catalog.js.map
package/dist/command.d.ts CHANGED
@@ -12,6 +12,12 @@ export interface Command {
12
12
  readonly details?: readonly string[];
13
13
  /** printed instead of the `zen` banner, so the sub-brand shows through */
14
14
  readonly banner?: BannerText;
15
+ /**
16
+ * True when this invocation's stdout is the answer rather than a report on
17
+ * it. The banner is then held back rather than dropped: if the run turns
18
+ * out to need a prompt, someone is there to see it, and it appears.
19
+ */
20
+ readonly quiet?: (args: readonly string[]) => boolean;
15
21
  /**
16
22
  * Answers `--help` itself, for a command whose arguments select a page the
17
23
  * frame cannot see. Without it the frame prints this command's own page.
@@ -1,3 +1,60 @@
1
1
  import type { Command } from '../command.ts';
2
+ import { type RunPaths, type SessionPaths } from '../session.ts';
3
+ import { type NodeDetail, type Trace, type TraceEntry } from '../trace.ts';
4
+ interface Flags {
5
+ project?: string;
6
+ session?: string;
7
+ run?: string;
8
+ dir?: string;
9
+ memory?: string;
10
+ model?: string;
11
+ part?: string[];
12
+ 'question-file'?: string;
13
+ full?: boolean;
14
+ open?: boolean;
15
+ rebuild?: boolean;
16
+ 'no-timing'?: boolean;
17
+ 'no-style'?: boolean;
18
+ }
2
19
  export declare const inspect: Command;
20
+ /**
21
+ * Which parts print in full: everything `--part` names, or everything but
22
+ * `request`. A prefix matches, so `--part call` catches `call run_command (…)`.
23
+ */
24
+ export declare function wholeParts(found: readonly NodeDetail[], values: {
25
+ part?: string[];
26
+ full?: boolean;
27
+ }): (name: string) => boolean;
28
+ /**
29
+ * One recorded call, put back to a model with a question on the end.
30
+ *
31
+ * `node` shows what the model was given; this asks the model what it made of
32
+ * it. Everything it sees is what it saw at the time, so an answer naming the
33
+ * skill that steered it is checkable against the same node.
34
+ */
35
+ export declare function replayableCalls(trace: Trace): {
36
+ entry: TraceEntry;
37
+ node: import("@zenera/neo").LlmCallNode;
38
+ }[];
39
+ export declare function renderAskExchange(question: string, answer: string, columns?: number): string[];
40
+ export declare function repeatQuestions(initial: string, next: () => Promise<string>, answer: (question: string) => Promise<void>): Promise<void>;
41
+ /** The question: the words on the line, or the whole of `--question-file` (`-` is stdin). */
42
+ export declare function readQuestion(words: readonly string[], file: string | undefined, cwd: string): Promise<string>;
43
+ /** One node, framed so a payload cannot be mistaken for the next node. */
44
+ export declare function renderNode(node: NodeDetail, whole?: (name: string) => boolean): string;
45
+ export interface Located {
46
+ project: string;
47
+ session: SessionPaths;
48
+ run: RunPaths;
49
+ }
50
+ /**
51
+ * Which run, by whichever handle the caller has.
52
+ *
53
+ * A directory is the handle a *program* holds: `zen run --json` reports one,
54
+ * and asking it to be taken apart into a project, a session and a run before
55
+ * it can be used again would be work for nothing. A person holds an id, or
56
+ * nothing at all and gets asked.
57
+ */
58
+ export declare function locate(cwd: string, values: Pick<Flags, 'project' | 'session' | 'run' | 'dir'>, positional: string | undefined, asking: boolean): Promise<Located>;
59
+ export {};
3
60
  //# sourceMappingURL=inspect.d.ts.map