@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
@@ -1,16 +1,60 @@
1
- import { createModel, readProjectConfig } from '@zenera/neo';
1
+ import { readProjectConfig } from '@zenera/neo';
2
+ import { spawn } from 'node:child_process';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { existsSync } from 'node:fs';
5
+ import { join, resolve } from 'node:path';
6
+ import { setTimeout as sleep } from 'node:timers/promises';
2
7
  import { parse } from "../args.js";
3
8
  import { loadProjectEnv } from "../env.js";
9
+ import { copilotEventLog } from "../eventlog.js";
4
10
  import { ensureHome } from "../home.js";
5
11
  import { assertOwner, KeyStore, PROVIDERS } from "../keys.js";
6
- import { probeModel } from "../liveness.js";
7
- import { answerBox, chooseModel, defaultRef, launch, listPrompts, loadPrompt, locate, masked, misspelledProvider, MODEL_ENV, openLog, ORDER, PROMPT_DIR, providersWarning, readMeta, RECOMMENDED, SOURCE_LABELS, splitRef, wire, writeMeta, } from "../meta.js";
12
+ import { probeChat, probeModel } from "../liveness.js";
13
+ import { answerBox, chooseModel, DATASET_DETAILS, DATASET_SUMMARY, DATASET_USAGE, datasetProjectFlag, DEFAULT_RESUMES, defaultRef, editorFiles, FINETUNE_DETAILS, FINETUNE_SUMMARY, FINETUNE_USAGE, finetuneProjectFlag, lastSession, launch, listPrompts, loadPrompt, locate, masked, misspelledProvider, MODEL_ENV, openLog, ORDER, PROMPT_DIR, providersWarning, readMeta, RECOMMENDED, recordSession, RESUME_PROMPT, resumeDelayMs, runDataset, runFinetune, sameModel, sessionModel, sessionTotals, SOURCE_LABELS, startVertexRelay, tailSpans, Tally, terminalSink, transient, wire, writeMeta, } from "../meta/index.js";
14
+ import { splitRef } from "../modelref.js";
15
+ import { duration } from "../narrate.js";
8
16
  import * as Projects from "../projects.js";
9
17
  import { project as resolveProject } from "../resolve.js";
10
- import { editorFiles } from "../scaffold.js";
11
18
  import { bold, choose, CliError, credentialError, cyan, dim, EXIT, green, isInteractive, json, note, progress, readStdin, table, usageError, warn, write, writeAll, } from "../term.js";
19
+ import { appendUsage, LEDGER_ENV, ledgerPath, META_PROMPT_ENV, META_SESSION_ENV, } from "../usage.js";
12
20
  const USAGE = 'zen meta run [project] [prompt] [options]';
13
- const VERBS = new Set(['model', 'run', 'prompts']);
21
+ const VERBS = new Set(['model', 'run', 'prompts', 'resume', 'dataset', 'finetune']);
22
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
23
+ /** A copilot session id, or the 7+ hex prefix of one it also accepts. */
24
+ const SESSION_ID = /^[0-9a-f]{7,}(?:-[0-9a-f]+)*$/i;
25
+ /** Verbs that own their flags and their help page. */
26
+ const SUBCOMMANDS = {
27
+ dataset: {
28
+ run: runDataset,
29
+ projectFlag: datasetProjectFlag,
30
+ page: { usage: DATASET_USAGE, summary: DATASET_SUMMARY, details: DATASET_DETAILS },
31
+ },
32
+ finetune: {
33
+ run: runFinetune,
34
+ projectFlag: finetuneProjectFlag,
35
+ page: { usage: FINETUNE_USAGE, summary: FINETUNE_SUMMARY, details: FINETUNE_DETAILS },
36
+ },
37
+ };
38
+ /** `zen meta dataset ...` or `zen meta acme dataset ...`: which one, and where its own args start. */
39
+ function subcommand(args) {
40
+ for (const name of Object.keys(SUBCOMMANDS)) {
41
+ if (args[0] === name) {
42
+ return { name, at: 0 };
43
+ }
44
+ if (args[1] === name && !args[0]?.startsWith('-')) {
45
+ return { name, at: 1 };
46
+ }
47
+ }
48
+ return undefined;
49
+ }
50
+ function page(p) {
51
+ write(bold(p.usage));
52
+ write(`\n ${p.summary}`);
53
+ write('');
54
+ for (const line of p.details) {
55
+ write(line ? ` ${line}` : '');
56
+ }
57
+ }
14
58
  export const meta = {
15
59
  summary: 'Run the meta agent over this project, on your own keys.',
16
60
  usage: USAGE,
@@ -21,9 +65,13 @@ export const meta = {
21
65
  ' zen meta run [project] /<name> [words] run a stored prompt',
22
66
  ' zen meta run [project] pick one of its stored prompts',
23
67
  ' zen meta prompts [project] list the stored prompts',
68
+ ' zen meta resume [project] [session] carry on a run that stopped',
24
69
  ' zen meta model [ref] show or set the model it uses',
70
+ ' zen meta dataset <verb> the cases it tunes against',
71
+ ' zen meta finetune <verb> tune the project on those cases',
25
72
  '',
26
73
  'The project may come before the verb instead: `zen meta acme run`.',
74
+ '`zen meta dataset --help` and `zen meta finetune --help` have their own pages.',
27
75
  '',
28
76
  'Arguments:',
29
77
  ' [project] Name of a project. Default: the one you are in.',
@@ -41,8 +89,14 @@ export const meta = {
41
89
  ' --ask Have it ask before each tool. Default: it does not.',
42
90
  ' --add-dir <dir> Another directory it may touch. Repeatable.',
43
91
  ' --resume <id> Continue a copilot session. --continue takes the last.',
92
+ ' --session-id <uuid> Start a new session under this id. Default: a random one.',
93
+ ` --retries <n> Resume after a rate limit or outage. Default ${DEFAULT_RESUMES}, 0 = off.`,
44
94
  ' --share <file> Write the transcript to a markdown file.',
95
+ ' --events <file> Append what the agent does to this file as it does it:',
96
+ ' one JSON line per model call, tool call and message.',
45
97
  ' --dry-run Print what would run, secrets masked, and stop.',
98
+ ' --no-refresh Leave the editor files as they are (parallel runs).',
99
+ ' --json Print one object: answer, answerFile, sessionId, exitCode, tokens.',
46
100
  '',
47
101
  'It always uses your own keys — `zen key add` — and never a coding-agent',
48
102
  'subscription. The answer goes to stdout and the progress to stderr, so',
@@ -53,6 +107,13 @@ export const meta = {
53
107
  'watching to answer. Narrow it with --allow-tool, or restore the asking',
54
108
  'with --ask.',
55
109
  '',
110
+ '`zen meta resume` picks up the last session the project ran - or the one',
111
+ 'named - where it stopped. A run that ends any way but success prints it.',
112
+ '',
113
+ 'Its tokens are counted as it runs and summed when it ends. In a project with',
114
+ 'a finetune/, every call - and every zen run and inspect ask it makes - is',
115
+ 'appended to finetune/usage/ledger.jsonl, and finetune/USAGE.md is rewritten.',
116
+ '',
56
117
  'Nothing it needs is put on a command line: every credential reaches it',
57
118
  'through the environment, where other processes cannot read it.',
58
119
  '',
@@ -75,12 +136,29 @@ export const meta = {
75
136
  ' zen meta prompts',
76
137
  ' zen meta run /project-review',
77
138
  ' zen meta run acme /spec-sync-project agents/triage.md',
139
+ ' zen meta run acme /analyze --json',
140
+ ' zen meta resume acme',
78
141
  ' git diff | zen meta run "what broke?" --allow-tool read',
79
142
  ' zen meta model vertex/gemini-3.8-flash',
80
143
  ' zen meta model --pick',
81
144
  ' zen meta run --dry-run "hello"',
82
145
  ],
146
+ help: (ctx) => {
147
+ const sub = subcommand(ctx.args);
148
+ page(sub ? SUBCOMMANDS[sub.name].page : { ...meta, details: meta.details ?? [] });
149
+ },
83
150
  run: async (ctx) => {
151
+ // These own their flags, so they are routed before the meta agent's are parsed.
152
+ const sub = subcommand(ctx.args);
153
+ if (sub) {
154
+ const { run, projectFlag } = SUBCOMMANDS[sub.name];
155
+ const args = ctx.args.slice(sub.at + 1);
156
+ const found = await resolveProject({
157
+ cwd: ctx.cwd,
158
+ project: projectFlag(args) ?? (sub.at === 1 ? ctx.args[0] : undefined),
159
+ });
160
+ return await run({ args, json: ctx.json, cwd: ctx.cwd }, found);
161
+ }
84
162
  const { values, positionals } = parse(ctx.args, {
85
163
  project: { type: 'string' },
86
164
  provider: { type: 'string' },
@@ -89,8 +167,11 @@ export const meta = {
89
167
  agent: { type: 'string' },
90
168
  effort: { type: 'string' },
91
169
  share: { type: 'string' },
170
+ events: { type: 'string' },
92
171
  resume: { type: 'string' },
93
172
  continue: { type: 'boolean' },
173
+ 'session-id': { type: 'string' },
174
+ retries: { type: 'string' },
94
175
  'allow-all': { type: 'boolean' },
95
176
  'allow-tool': { type: 'string' },
96
177
  ask: { type: 'boolean' },
@@ -100,6 +181,7 @@ export const meta = {
100
181
  pick: { type: 'boolean' },
101
182
  clear: { type: 'boolean' },
102
183
  force: { type: 'boolean' },
184
+ 'no-refresh': { type: 'boolean' },
103
185
  }, USAGE);
104
186
  // `zen meta acme run` is typed at least as often as `zen meta run acme`,
105
187
  // and read literally it asks the agent the one-word question "run" —
@@ -126,6 +208,9 @@ export const meta = {
126
208
  if (verb === 'prompts') {
127
209
  return await prompts(ctx, values, args);
128
210
  }
211
+ if (verb === 'resume') {
212
+ return await resumeRun(ctx, values, args);
213
+ }
129
214
  throw usageError(first === undefined ? 'nothing to run' : 'a prompt goes through `run`', `try: zen meta run ${positionals.map(requote).join(' ') || '"what does this project do?"'}`);
130
215
  },
131
216
  };
@@ -191,7 +276,9 @@ async function stored(ctx, values, args) {
191
276
  rest = args;
192
277
  }
193
278
  const { project } = await where(ctx, values.project, head);
194
- refresh(project);
279
+ if (!values['no-refresh']) {
280
+ refresh(project);
281
+ }
195
282
  if (name) {
196
283
  return await runPrompt(ctx, values, project, name, rest);
197
284
  }
@@ -208,7 +295,7 @@ async function runPrompt(ctx, values, project, name, extra) {
208
295
  note(dim(found.description));
209
296
  }
210
297
  const prompt = extra.length > 0 ? `${found.body}\n\n${extra.join(' ')}` : found.body;
211
- await go(ctx, values, project, prompt);
298
+ await go(ctx, values, project, prompt, name.replace(/^\//, ''));
212
299
  }
213
300
  async function pickPrompt(ctx, project) {
214
301
  const names = await listPrompts(project.dir);
@@ -243,9 +330,40 @@ async function prompts(ctx, values, args) {
243
330
  note(dim(`run one: zen meta run /${found[0].name}`));
244
331
  }
245
332
  // ---------------------------------------------------------------------------
333
+ // zen meta resume [project] [session id] [words...]
334
+ //
335
+ // A long run stops for reasons nobody chose - a refused request, a closed
336
+ // laptop, Ctrl-C. Copilot keeps the session; this hands it back, by default the
337
+ // last one the project ran, with the words (or a plain "continue") as the turn.
338
+ // ---------------------------------------------------------------------------
339
+ async function resumeRun(ctx, values, args) {
340
+ const { project, rest } = await where(ctx, values.project, args);
341
+ const named = rest[0] !== undefined && SESSION_ID.test(rest[0]) ? rest[0] : undefined;
342
+ const words = named ? rest.slice(1) : rest;
343
+ const session = named ?? values.resume ?? lastSession(project.dir);
344
+ if (!session) {
345
+ throw usageError(`no meta session recorded in ${project.name}`, `name one: zen meta resume ${project.name} <session id>`);
346
+ }
347
+ note(dim(`resuming session ${session}`));
348
+ if (!values['no-refresh']) {
349
+ refresh(project);
350
+ }
351
+ const message = (values.prompt ?? words.join(' ')).trim() || RESUME_PROMPT;
352
+ await go(ctx, { ...values, resume: session, continue: false }, project, message);
353
+ }
354
+ // ---------------------------------------------------------------------------
246
355
  // The run itself
247
356
  // ---------------------------------------------------------------------------
248
- async function go(ctx, values, project, prompt) {
357
+ async function go(ctx, values, project, prompt, promptName) {
358
+ const fresh = values['session-id'];
359
+ if (fresh !== undefined) {
360
+ if (values.resume || values.continue) {
361
+ throw usageError('--session-id starts a new session', 'drop --resume / --continue');
362
+ }
363
+ if (!UUID.test(fresh)) {
364
+ throw usageError(`--session-id must be a UUID, not ${fresh}`, 'try: uuidgen');
365
+ }
366
+ }
249
367
  const shell = process.env[MODEL_ENV];
250
368
  loadProjectEnv(project.dir);
251
369
  ensureHome();
@@ -280,7 +398,19 @@ async function go(ctx, values, project, prompt) {
280
398
  }
281
399
  const wiring = wire(store, entry, id);
282
400
  const binary = locate();
283
- const args = argv(values, project.dir, prompt, wiring.secret);
401
+ // Copilot cannot carry a session to another model: the resume fails on the first call.
402
+ const ran = values.resume ? sessionModel(values.resume) : undefined;
403
+ if (values.resume && ran && !sameModel(ran, wiring.model)) {
404
+ if (prompt === RESUME_PROMPT) {
405
+ throw usageError(`session ${values.resume} ran on ${ran}, and the meta model is now ${wiring.model} - copilot cannot carry a session to another model`, `switch back for it: zen meta model ${owner}/${ran.replace(/^.*\//, '')} - or ask again, which starts a new session`);
406
+ }
407
+ warn(`session ${values.resume} ran on ${ran}; the meta model is now ${wiring.model}, so this starts a new session`);
408
+ values = { ...values, resume: undefined };
409
+ }
410
+ // Named up front, so a run killed before copilot reports its id can still be resumed.
411
+ let session = values.resume ?? (values.continue ? undefined : (fresh ?? randomUUID()));
412
+ const args = argv(values, project.dir, prompt, wiring.secret, session);
413
+ const retries = count(values.retries, '--retries') ?? DEFAULT_RESUMES;
284
414
  if (values['dry-run']) {
285
415
  const lines = masked(wiring.env, wiring.secret);
286
416
  if (ctx.json) {
@@ -310,33 +440,112 @@ async function go(ctx, values, project, prompt) {
310
440
  note(dim('set another: zen meta model <provider>/<id>'));
311
441
  }
312
442
  note(cyan(`${owner} · ${wiring.model} · ${project.name}`));
313
- const log = openLog(project.dir);
443
+ const log = openLog(project.dir, new Date(), session);
314
444
  note(`log ${log.path}`);
315
445
  const elided = args.map((a, i) => (args[i - 1] === '-p' ? '<prompt>' : a));
316
446
  log.line(`zen meta ${new Date().toISOString()}`);
317
447
  log.line(`project ${project.name} ${project.dir}`);
318
448
  log.line(`model ${chosen.ref} from ${SOURCE_LABELS[chosen.from]}`);
319
449
  log.line(`command ${[binary.command, ...binary.args, ...elided].join(' ')}`);
450
+ if (session) {
451
+ recordSession(project.dir, session);
452
+ log.line(`session ${session}`);
453
+ }
454
+ const relay = wiring.relay ? await startVertexRelay(wiring.relay) : undefined;
455
+ if (relay) {
456
+ wiring.env.COPILOT_PROVIDER_BASE_URL = relay.url;
457
+ log.line(`relay ${relay.url} -> vertex rawPredict`);
458
+ }
320
459
  log.line('');
321
460
  log.line('--- prompt ---');
322
461
  log.line(prompt);
323
462
  log.line('');
324
463
  log.line('--- run ---');
464
+ const resumeHint = () => session ? `resume it: zen meta resume ${project.name} ${session}` : undefined;
465
+ // Every zen command the agent runs inherits these, so a batch it starts or
466
+ // an ask it makes lands in the same ledger, tagged with this session.
467
+ const spans = log.path.replace(/\.log$/, '.otel.jsonl');
468
+ const ledger = ledgerPath(project.dir);
469
+ const env = {
470
+ ...wiring.env,
471
+ COPILOT_OTEL_FILE_EXPORTER_PATH: spans,
472
+ ...(ledger ? { [LEDGER_ENV]: ledger } : {}),
473
+ ...(session ? { [META_SESSION_ENV]: session } : {}),
474
+ ...(promptName ? { [META_PROMPT_ENV]: promptName } : {}),
475
+ };
476
+ const tally = new Tally();
477
+ const record = values.events ? copilotEventLog(resolve(ctx.cwd, values.events)) : undefined;
478
+ const tail = tailSpans(spans, (call) => {
479
+ tally.add(call);
480
+ record?.call(call);
481
+ appendUsage(project.dir, {
482
+ kind: 'meta.call',
483
+ ts: new Date(Date.parse(call.startedAt) + call.durationMs).toISOString(),
484
+ ...call,
485
+ session: call.session ?? session,
486
+ });
487
+ });
488
+ const reporter = usageReporter(project.dir);
489
+ const tokens = () => {
490
+ tail.stop();
491
+ return tally.toString();
492
+ };
325
493
  // A run that dies still has to leave a readable file behind.
326
494
  try {
327
- const outcome = await launch({
328
- binary,
329
- args,
330
- env: wiring.env,
331
- cwd: project.dir,
332
- log,
333
- });
495
+ let resumes = 0;
496
+ const run = async (argList) => {
497
+ const launchedAt = new Date();
498
+ const outcome = await launch({
499
+ binary,
500
+ args: argList,
501
+ env,
502
+ cwd: project.dir,
503
+ log,
504
+ sink: ctx.json ? terminalSink : undefined,
505
+ onEvent: record?.event,
506
+ status: () => (tally.calls > 0 ? tally.toString() : ''),
507
+ });
508
+ if (outcome.sessionId && outcome.sessionId !== session) {
509
+ session = outcome.sessionId;
510
+ recordSession(project.dir, session);
511
+ }
512
+ const models = session ? sessionTotals(session) : undefined;
513
+ if (session && models) {
514
+ appendUsage(project.dir, {
515
+ kind: 'meta.session',
516
+ ts: new Date().toISOString(),
517
+ session,
518
+ models,
519
+ exitCode: outcome.exitCode,
520
+ resumes,
521
+ startedAt: launchedAt.toISOString(),
522
+ durationMs: Date.now() - launchedAt.getTime(),
523
+ });
524
+ }
525
+ return outcome;
526
+ };
527
+ let outcome = await run(args);
528
+ for (let reason = transient(outcome); reason && resumes < retries;) {
529
+ const wait = resumeDelayMs(resumes);
530
+ resumes += 1;
531
+ const said = `${reason}: resuming in ${duration(wait)} (${resumes}/${retries})`;
532
+ warn(said);
533
+ log.line('');
534
+ log.line(`--- resume ${resumes}: ${said} ---`);
535
+ await pause(wait, resumeHint());
536
+ const again = { ...values, resume: session, continue: false };
537
+ outcome = await run(argv(again, project.dir, RESUME_PROMPT, wiring.secret));
538
+ reason = transient(outcome);
539
+ }
334
540
  log.line('');
335
541
  log.line('--- answer ---');
336
542
  log.line(outcome.answer);
337
543
  if (outcome.answer) {
338
544
  log.saveAnswer(outcome.answer);
339
545
  }
546
+ const spent = tokens();
547
+ log.line('');
548
+ log.line(`tokens ${spent}`);
340
549
  if (ctx.json) {
341
550
  json({
342
551
  project: project.dir,
@@ -344,7 +553,9 @@ async function go(ctx, values, project, prompt) {
344
553
  model: chosen.ref,
345
554
  sessionId: outcome.sessionId,
346
555
  exitCode: outcome.exitCode,
556
+ resumes,
347
557
  usage: outcome.usage,
558
+ tokens: { calls: tally.calls, ...tally.usage },
348
559
  answer: outcome.answer,
349
560
  answerFile: outcome.answer ? log.answerPath : undefined,
350
561
  });
@@ -355,16 +566,82 @@ async function go(ctx, values, project, prompt) {
355
566
  // have read enough to want it in an editor.
356
567
  note(cyan(log.answerPath));
357
568
  }
569
+ if (!ctx.json && tally.calls > 0) {
570
+ note(dim(`tokens ${spent}`));
571
+ }
358
572
  if (outcome.exitCode !== 0) {
359
- throw new CliError(`the meta agent exited ${outcome.exitCode}`, EXIT.failed, binary.from === 'npx' ? 'install it: npm i -g @github/copilot' : undefined);
573
+ throw new CliError(`the meta agent exited ${outcome.exitCode}`, EXIT.failed, resumeHint() ??
574
+ (binary.from === 'npx' ? 'install it: npm i -g @github/copilot' : undefined));
360
575
  }
361
576
  }
577
+ catch (err) {
578
+ const hint = resumeHint();
579
+ if (hint && !(err instanceof CliError)) {
580
+ note(dim(hint));
581
+ }
582
+ throw err;
583
+ }
362
584
  finally {
585
+ tail.stop();
586
+ reporter.stop();
587
+ await relay?.close();
363
588
  log.close();
364
589
  }
365
590
  }
591
+ /** How often a running meta agent rewrites the fine-tuning's usage report. */
592
+ const REPORT_EVERY_MS = 5 * 60_000;
593
+ /**
594
+ * Rewrites `finetune/USAGE.md` on a timer and once more at the end, when the
595
+ * project is being tuned. Detached and silent: the report is a by-product, and
596
+ * neither its time nor its failure belongs to the run.
597
+ */
598
+ function usageReporter(dir) {
599
+ const script = join(dir, '.github/skills/zen-finetune/scripts/usage.mjs');
600
+ if (!existsSync(join(dir, 'finetune')) || !existsSync(script)) {
601
+ return { stop: () => undefined };
602
+ }
603
+ let running = false;
604
+ const refresh = () => {
605
+ if (running) {
606
+ return;
607
+ }
608
+ running = true;
609
+ const child = spawn(process.execPath, [script, '-q'], {
610
+ cwd: dir,
611
+ stdio: 'ignore',
612
+ detached: true,
613
+ });
614
+ child.on('exit', () => (running = false));
615
+ child.on('error', () => (running = false));
616
+ child.unref();
617
+ };
618
+ const timer = setInterval(refresh, REPORT_EVERY_MS);
619
+ timer.unref();
620
+ return {
621
+ stop: () => {
622
+ clearInterval(timer);
623
+ running = false;
624
+ refresh();
625
+ },
626
+ };
627
+ }
628
+ /** The wait before a resume, cut short by Ctrl-C - which still says how to resume. */
629
+ async function pause(ms, hint) {
630
+ const stop = new AbortController();
631
+ const onInt = () => stop.abort();
632
+ process.once('SIGINT', onInt);
633
+ try {
634
+ await sleep(ms, undefined, { signal: stop.signal });
635
+ }
636
+ catch {
637
+ throw new CliError('interrupted', EXIT.failed, hint);
638
+ }
639
+ finally {
640
+ process.off('SIGINT', onInt);
641
+ }
642
+ }
366
643
  /** Copilot's own flags, assembled once. Nothing secret goes on this line. */
367
- function argv(values, dir, prompt, secret) {
644
+ function argv(values, dir, prompt, secret, fresh) {
368
645
  const args = ['-C', dir, '-p', prompt, '--output-format', 'json', '--no-color'];
369
646
  // A prompt written for an editor expects to read, write and run things, and
370
647
  // there is nobody at a `-p` run to answer the question. Naming tools is the
@@ -390,9 +667,12 @@ function argv(values, dir, prompt, secret) {
390
667
  if (values.resume) {
391
668
  args.push('--resume', values.resume);
392
669
  }
393
- if (values.continue) {
670
+ else if (values.continue) {
394
671
  args.push('--continue');
395
672
  }
673
+ else if (fresh) {
674
+ args.push('--session-id', fresh);
675
+ }
396
676
  if (secret.length > 0) {
397
677
  args.push('--secret-env-vars', secret.join(','));
398
678
  }
@@ -417,6 +697,16 @@ function projectModel(dir) {
417
697
  return undefined;
418
698
  }
419
699
  }
700
+ function count(text, flag) {
701
+ if (text === undefined) {
702
+ return undefined;
703
+ }
704
+ const value = Number(text);
705
+ if (!Number.isInteger(value) || value < 0) {
706
+ throw usageError(`${flag} takes a whole number, 0 or more`, `got "${text}"`);
707
+ }
708
+ return value;
709
+ }
420
710
  function asProvider(name) {
421
711
  const owner = assertOwner(name);
422
712
  if (owner === 'exa') {
@@ -534,7 +824,7 @@ async function vet(ctx, ref) {
534
824
  // The colon form, so a refusal suggests `zen models ls <provider>`.
535
825
  ref: `${owner}:${id}`,
536
826
  kind: 'model',
537
- model: createModel({ provider: owner, model: id, maxTokens: 16 }),
827
+ model: probeChat(owner, id),
538
828
  });
539
829
  bar?.done();
540
830
  if (probe.check.state !== 'live') {