@zenera/cli 1.1.24 → 1.1.26

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 (199) hide show
  1. package/README.md +65 -0
  2. package/dist/ask.js +28 -5
  3. package/dist/banner.js +2 -2
  4. package/dist/batch.d.ts +6 -0
  5. package/dist/batch.js +76 -1
  6. package/dist/catalog.d.ts +31 -1
  7. package/dist/catalog.js +140 -7
  8. package/dist/commands/index.js +2 -0
  9. package/dist/commands/inspect.d.ts +52 -2
  10. package/dist/commands/inspect.js +220 -31
  11. package/dist/commands/memory.js +156 -32
  12. package/dist/commands/meta.js +333 -24
  13. package/dist/commands/models.js +97 -12
  14. package/dist/commands/open.js +4 -4
  15. package/dist/commands/run.js +38 -4
  16. package/dist/commands/sandbox.js +148 -16
  17. package/dist/commands/view.d.ts +8 -0
  18. package/dist/commands/view.js +114 -0
  19. package/dist/engine.d.ts +8 -1
  20. package/dist/engine.js +7 -0
  21. package/dist/eventlog.d.ts +64 -0
  22. package/dist/eventlog.js +170 -0
  23. package/dist/liveness.d.ts +7 -0
  24. package/dist/liveness.js +35 -3
  25. package/dist/main.js +0 -0
  26. package/dist/meta/editor.d.ts +9 -0
  27. package/dist/meta/editor.js +43 -0
  28. package/dist/meta/finetune/analyze-brief.d.ts +26 -0
  29. package/dist/meta/finetune/analyze-brief.js +131 -0
  30. package/dist/meta/finetune/apply-brief.d.ts +23 -0
  31. package/dist/meta/finetune/apply-brief.js +247 -0
  32. package/dist/meta/finetune/brief.d.ts +45 -0
  33. package/dist/meta/finetune/brief.js +158 -0
  34. package/dist/meta/finetune/children.d.ts +8 -0
  35. package/dist/meta/finetune/children.js +76 -0
  36. package/dist/meta/finetune/command.d.ts +30 -0
  37. package/dist/meta/finetune/command.js +516 -0
  38. package/dist/meta/finetune/cost.d.ts +46 -0
  39. package/dist/meta/finetune/cost.js +105 -0
  40. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  41. package/dist/meta/finetune/dataset/anchors.js +239 -0
  42. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  43. package/dist/meta/finetune/dataset/changes.js +319 -0
  44. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  45. package/dist/meta/finetune/dataset/command.js +1011 -0
  46. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  47. package/dist/meta/finetune/dataset/drift.js +71 -0
  48. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  49. package/dist/meta/finetune/dataset/export.js +51 -0
  50. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  51. package/dist/meta/finetune/dataset/index.js +2 -0
  52. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  53. package/dist/meta/finetune/dataset/sample.js +197 -0
  54. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  55. package/dist/meta/finetune/dataset/schema.js +216 -0
  56. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  57. package/dist/meta/finetune/dataset/store.js +177 -0
  58. package/dist/meta/finetune/dataset/types.d.ts +127 -0
  59. package/dist/meta/finetune/dataset/types.js +39 -0
  60. package/dist/meta/finetune/failure.d.ts +15 -0
  61. package/dist/meta/finetune/failure.js +26 -0
  62. package/dist/meta/finetune/feedback.d.ts +57 -0
  63. package/dist/meta/finetune/feedback.js +104 -0
  64. package/dist/meta/finetune/guard.d.ts +29 -0
  65. package/dist/meta/finetune/guard.js +104 -0
  66. package/dist/meta/finetune/improvements.d.ts +98 -0
  67. package/dist/meta/finetune/improvements.js +501 -0
  68. package/dist/meta/finetune/index.d.ts +2 -0
  69. package/dist/meta/finetune/index.js +2 -0
  70. package/dist/meta/finetune/journal.d.ts +35 -0
  71. package/dist/meta/finetune/journal.js +135 -0
  72. package/dist/meta/finetune/live.d.ts +84 -0
  73. package/dist/meta/finetune/live.js +272 -0
  74. package/dist/meta/finetune/loop.d.ts +3 -0
  75. package/dist/meta/finetune/loop.js +163 -0
  76. package/dist/meta/finetune/memories.d.ts +47 -0
  77. package/dist/meta/finetune/memories.js +154 -0
  78. package/dist/meta/finetune/recheck.d.ts +58 -0
  79. package/dist/meta/finetune/recheck.js +311 -0
  80. package/dist/meta/finetune/report.d.ts +40 -0
  81. package/dist/meta/finetune/report.js +1397 -0
  82. package/dist/meta/finetune/retry.d.ts +24 -0
  83. package/dist/meta/finetune/retry.js +96 -0
  84. package/dist/meta/finetune/sampler.d.ts +18 -0
  85. package/dist/meta/finetune/sampler.js +47 -0
  86. package/dist/meta/finetune/system.d.ts +27 -0
  87. package/dist/meta/finetune/system.js +132 -0
  88. package/dist/meta/finetune/train.d.ts +43 -0
  89. package/dist/meta/finetune/train.js +425 -0
  90. package/dist/meta/finetune/tui.d.ts +16 -0
  91. package/dist/meta/finetune/tui.js +316 -0
  92. package/dist/meta/finetune/tuning.d.ts +193 -0
  93. package/dist/meta/finetune/tuning.js +308 -0
  94. package/dist/meta/finetune/usage.d.ts +66 -0
  95. package/dist/meta/finetune/usage.js +260 -0
  96. package/dist/meta/finetune/view.d.ts +9 -0
  97. package/dist/meta/finetune/view.js +27 -0
  98. package/dist/meta/host.d.ts +15 -0
  99. package/dist/meta/host.js +21 -0
  100. package/dist/meta/index.d.ts +11 -0
  101. package/dist/meta/index.js +11 -0
  102. package/dist/meta/launch.d.ts +81 -0
  103. package/dist/meta/launch.js +286 -0
  104. package/dist/meta/model.d.ts +44 -0
  105. package/dist/meta/model.js +119 -0
  106. package/dist/meta/prompts.d.ts +18 -0
  107. package/dist/meta/prompts.js +61 -0
  108. package/dist/meta/relay.d.ts +20 -0
  109. package/dist/meta/relay.js +123 -0
  110. package/dist/meta/tokens.d.ts +52 -0
  111. package/dist/meta/tokens.js +226 -0
  112. package/dist/meta/window.d.ts +61 -0
  113. package/dist/meta/window.js +198 -0
  114. package/dist/meta/wire.d.ts +42 -0
  115. package/dist/meta/wire.js +129 -0
  116. package/dist/meta.d.ts +71 -1
  117. package/dist/meta.js +303 -6
  118. package/dist/modelref.d.ts +16 -0
  119. package/dist/modelref.js +41 -0
  120. package/dist/narrate.js +5 -2
  121. package/dist/podman.d.ts +26 -0
  122. package/dist/podman.js +83 -3
  123. package/dist/projects.d.ts +0 -5
  124. package/dist/projects.js +2 -15
  125. package/dist/resources.d.ts +215 -0
  126. package/dist/resources.js +856 -0
  127. package/dist/sandbox.d.ts +2 -0
  128. package/dist/sandbox.js +10 -3
  129. package/dist/scaffold.d.ts +15 -7
  130. package/dist/scaffold.js +24 -67
  131. package/dist/session.js +4 -29
  132. package/dist/term.d.ts +2 -0
  133. package/dist/term.js +11 -2
  134. package/dist/trace.d.ts +5 -0
  135. package/dist/trace.js +107 -7
  136. package/dist/tui/app.js +19 -5
  137. package/dist/tui/links.d.ts +27 -0
  138. package/dist/tui/links.js +142 -0
  139. package/dist/tui/models.d.ts +35 -0
  140. package/dist/tui/models.js +447 -0
  141. package/dist/tui/sandbox.d.ts +10 -0
  142. package/dist/tui/sandbox.js +131 -0
  143. package/dist/tui/wrap.d.ts +5 -0
  144. package/dist/tui/wrap.js +12 -0
  145. package/dist/usage.d.ts +78 -0
  146. package/dist/usage.js +51 -0
  147. package/package.json +4 -2
  148. package/templates/editor/.github/copilot-instructions.md +60 -145
  149. package/templates/editor/.github/prompts/analyze.prompt.md +33 -0
  150. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  151. package/templates/editor/.github/prompts/finetune-apply.prompt.md +270 -0
  152. package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
  153. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  154. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  155. package/templates/editor/.github/prompts/project-review.prompt.md +5 -4
  156. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +588 -0
  157. package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
  158. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  159. package/templates/editor/.github/skills/zen-cli/references/finetune.md +137 -0
  160. package/templates/editor/.github/skills/zen-cli/references/inspect.md +63 -11
  161. package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
  162. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  163. package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
  164. package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
  165. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  166. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +35 -14
  167. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  168. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  169. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  170. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1492 -660
  171. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  172. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  173. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  174. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  175. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  176. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  177. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  178. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  179. package/templates/editor/.github/skills/zen-inspect/SKILL.md +74 -26
  180. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +271 -0
  181. package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
  182. package/templates/editor/.github/skills/zen-memory/SKILL.md +82 -53
  183. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  184. package/templates/editor/.github/skills/zen-review/SKILL.md +8 -31
  185. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -5
  186. package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
  187. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +9 -4
  188. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
  189. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
  190. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  191. package/templates/editor/.vscode/settings.json +1 -0
  192. package/templates/project/agents/files-instructions.md +29 -20
  193. package/templates/project/agents/fork-instructions.md +78 -28
  194. package/templates/project/agents/memory-instructions.md +222 -494
  195. package/templates/project/agents/tools-instructions.md +14 -4
  196. package/templates/project/gitignore +2 -0
  197. package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
  198. package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
  199. package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
@@ -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, readFileSync } 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.',
@@ -35,14 +83,21 @@ export const meta = {
35
83
  ' --project <name|dir> Which project to work in. The agent is rooted there.',
36
84
  ' --model <ref> Model for this run, e.g. vertex/gemini-3.8-flash.',
37
85
  ' --provider <name> Force the key to use. Default: the model ref says.',
86
+ ' --prompt-file <file> Append this file to the prompt, after any words.',
38
87
  ' --agent <name> A .github/agents/<name>.agent.md.',
39
88
  ' --effort <level> none, minimal, low, medium, high, xhigh or max.',
40
89
  ' --allow-tool <list> Only these: read, shell(git:*), write(README.md).',
41
90
  ' --ask Have it ask before each tool. Default: it does not.',
42
91
  ' --add-dir <dir> Another directory it may touch. Repeatable.',
43
92
  ' --resume <id> Continue a copilot session. --continue takes the last.',
93
+ ' --session-id <uuid> Start a new session under this id. Default: a random one.',
94
+ ` --retries <n> Resume after a rate limit or outage. Default ${DEFAULT_RESUMES}, 0 = off.`,
44
95
  ' --share <file> Write the transcript to a markdown file.',
96
+ ' --events <file> Append what the agent does to this file as it does it:',
97
+ ' one JSON line per model call, tool call and message.',
45
98
  ' --dry-run Print what would run, secrets masked, and stop.',
99
+ ' --no-refresh Leave the editor files as they are (parallel runs).',
100
+ ' --json Print one object: answer, answerFile, sessionId, exitCode, tokens.',
46
101
  '',
47
102
  'It always uses your own keys — `zen key add` — and never a coding-agent',
48
103
  'subscription. The answer goes to stdout and the progress to stderr, so',
@@ -53,6 +108,13 @@ export const meta = {
53
108
  'watching to answer. Narrow it with --allow-tool, or restore the asking',
54
109
  'with --ask.',
55
110
  '',
111
+ '`zen meta resume` picks up the last session the project ran - or the one',
112
+ 'named - where it stopped. A run that ends any way but success prints it.',
113
+ '',
114
+ 'Its tokens are counted as it runs and summed when it ends. In a project with',
115
+ 'a finetune/, every call - and every zen run and inspect ask it makes - is',
116
+ 'appended to finetune/usage/ledger.jsonl, and finetune/USAGE.md is rewritten.',
117
+ '',
56
118
  'Nothing it needs is put on a command line: every credential reaches it',
57
119
  'through the environment, where other processes cannot read it.',
58
120
  '',
@@ -75,22 +137,43 @@ export const meta = {
75
137
  ' zen meta prompts',
76
138
  ' zen meta run /project-review',
77
139
  ' zen meta run acme /spec-sync-project agents/triage.md',
140
+ ' zen meta run acme /analyze --json',
141
+ ' zen meta resume acme',
78
142
  ' git diff | zen meta run "what broke?" --allow-tool read',
79
143
  ' zen meta model vertex/gemini-3.8-flash',
80
144
  ' zen meta model --pick',
81
145
  ' zen meta run --dry-run "hello"',
82
146
  ],
147
+ help: (ctx) => {
148
+ const sub = subcommand(ctx.args);
149
+ page(sub ? SUBCOMMANDS[sub.name].page : { ...meta, details: meta.details ?? [] });
150
+ },
83
151
  run: async (ctx) => {
152
+ // These own their flags, so they are routed before the meta agent's are parsed.
153
+ const sub = subcommand(ctx.args);
154
+ if (sub) {
155
+ const { run, projectFlag } = SUBCOMMANDS[sub.name];
156
+ const args = ctx.args.slice(sub.at + 1);
157
+ const found = await resolveProject({
158
+ cwd: ctx.cwd,
159
+ project: projectFlag(args) ?? (sub.at === 1 ? ctx.args[0] : undefined),
160
+ });
161
+ return await run({ args, json: ctx.json, cwd: ctx.cwd }, found);
162
+ }
84
163
  const { values, positionals } = parse(ctx.args, {
85
164
  project: { type: 'string' },
86
165
  provider: { type: 'string' },
87
166
  model: { type: 'string' },
88
167
  prompt: { type: 'string', short: 'p' },
168
+ 'prompt-file': { type: 'string' },
89
169
  agent: { type: 'string' },
90
170
  effort: { type: 'string' },
91
171
  share: { type: 'string' },
172
+ events: { type: 'string' },
92
173
  resume: { type: 'string' },
93
174
  continue: { type: 'boolean' },
175
+ 'session-id': { type: 'string' },
176
+ retries: { type: 'string' },
94
177
  'allow-all': { type: 'boolean' },
95
178
  'allow-tool': { type: 'string' },
96
179
  ask: { type: 'boolean' },
@@ -100,6 +183,7 @@ export const meta = {
100
183
  pick: { type: 'boolean' },
101
184
  clear: { type: 'boolean' },
102
185
  force: { type: 'boolean' },
186
+ 'no-refresh': { type: 'boolean' },
103
187
  }, USAGE);
104
188
  // `zen meta acme run` is typed at least as often as `zen meta run acme`,
105
189
  // and read literally it asks the agent the one-word question "run" —
@@ -126,6 +210,9 @@ export const meta = {
126
210
  if (verb === 'prompts') {
127
211
  return await prompts(ctx, values, args);
128
212
  }
213
+ if (verb === 'resume') {
214
+ return await resumeRun(ctx, values, args);
215
+ }
129
216
  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
217
  },
131
218
  };
@@ -158,7 +245,9 @@ async function where(ctx, flag, positionals) {
158
245
  function refresh(project) {
159
246
  try {
160
247
  const written = editorFiles(project.dir);
161
- note(dim(`refreshed ${written.length} editor files in ${project.name}`));
248
+ if (written.length) {
249
+ note(dim(`refreshed ${written.length} editor files in ${project.name}`));
250
+ }
162
251
  }
163
252
  catch (err) {
164
253
  warn(`could not refresh the editor files — ${err.message}`);
@@ -191,11 +280,13 @@ async function stored(ctx, values, args) {
191
280
  rest = args;
192
281
  }
193
282
  const { project } = await where(ctx, values.project, head);
194
- refresh(project);
283
+ if (!values['no-refresh']) {
284
+ refresh(project);
285
+ }
195
286
  if (name) {
196
287
  return await runPrompt(ctx, values, project, name, rest);
197
288
  }
198
- const question = (values.prompt ?? rest.join(' ')).trim() || (await readStdin());
289
+ const question = withFile(values, (values.prompt ?? rest.join(' ')).trim()) || (await readStdin());
199
290
  if (question) {
200
291
  return await go(ctx, values, project, question);
201
292
  }
@@ -207,8 +298,23 @@ async function runPrompt(ctx, values, project, name, extra) {
207
298
  if (found.description) {
208
299
  note(dim(found.description));
209
300
  }
210
- const prompt = extra.length > 0 ? `${found.body}\n\n${extra.join(' ')}` : found.body;
211
- await go(ctx, values, project, prompt);
301
+ const prompt = withFile(values, extra.length > 0 ? `${found.body}\n\n${extra.join(' ')}` : found.body);
302
+ await go(ctx, values, project, prompt, name.replace(/^\//, ''));
303
+ }
304
+ /** The prompt with `--prompt-file` after it: text too long, or too awkward to quote, for a word. */
305
+ function withFile(values, prompt) {
306
+ const file = values['prompt-file'];
307
+ if (!file) {
308
+ return prompt;
309
+ }
310
+ let text;
311
+ try {
312
+ text = readFileSync(resolve(file), 'utf8').trim();
313
+ }
314
+ catch {
315
+ throw usageError(`no prompt file at ${file}`, 'name a file that exists');
316
+ }
317
+ return [prompt, text].filter(Boolean).join('\n\n');
212
318
  }
213
319
  async function pickPrompt(ctx, project) {
214
320
  const names = await listPrompts(project.dir);
@@ -243,9 +349,40 @@ async function prompts(ctx, values, args) {
243
349
  note(dim(`run one: zen meta run /${found[0].name}`));
244
350
  }
245
351
  // ---------------------------------------------------------------------------
352
+ // zen meta resume [project] [session id] [words...]
353
+ //
354
+ // A long run stops for reasons nobody chose - a refused request, a closed
355
+ // laptop, Ctrl-C. Copilot keeps the session; this hands it back, by default the
356
+ // last one the project ran, with the words (or a plain "continue") as the turn.
357
+ // ---------------------------------------------------------------------------
358
+ async function resumeRun(ctx, values, args) {
359
+ const { project, rest } = await where(ctx, values.project, args);
360
+ const named = rest[0] !== undefined && SESSION_ID.test(rest[0]) ? rest[0] : undefined;
361
+ const words = named ? rest.slice(1) : rest;
362
+ const session = named ?? values.resume ?? lastSession(project.dir);
363
+ if (!session) {
364
+ throw usageError(`no meta session recorded in ${project.name}`, `name one: zen meta resume ${project.name} <session id>`);
365
+ }
366
+ note(dim(`resuming session ${session}`));
367
+ if (!values['no-refresh']) {
368
+ refresh(project);
369
+ }
370
+ const message = withFile(values, (values.prompt ?? words.join(' ')).trim()) || RESUME_PROMPT;
371
+ await go(ctx, { ...values, resume: session, continue: false }, project, message);
372
+ }
373
+ // ---------------------------------------------------------------------------
246
374
  // The run itself
247
375
  // ---------------------------------------------------------------------------
248
- async function go(ctx, values, project, prompt) {
376
+ async function go(ctx, values, project, prompt, promptName) {
377
+ const fresh = values['session-id'];
378
+ if (fresh !== undefined) {
379
+ if (values.resume || values.continue) {
380
+ throw usageError('--session-id starts a new session', 'drop --resume / --continue');
381
+ }
382
+ if (!UUID.test(fresh)) {
383
+ throw usageError(`--session-id must be a UUID, not ${fresh}`, 'try: uuidgen');
384
+ }
385
+ }
249
386
  const shell = process.env[MODEL_ENV];
250
387
  loadProjectEnv(project.dir);
251
388
  ensureHome();
@@ -280,7 +417,19 @@ async function go(ctx, values, project, prompt) {
280
417
  }
281
418
  const wiring = wire(store, entry, id);
282
419
  const binary = locate();
283
- const args = argv(values, project.dir, prompt, wiring.secret);
420
+ // Copilot cannot carry a session to another model: the resume fails on the first call.
421
+ const ran = values.resume ? sessionModel(values.resume) : undefined;
422
+ if (values.resume && ran && !sameModel(ran, wiring.model)) {
423
+ if (prompt === RESUME_PROMPT) {
424
+ 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`);
425
+ }
426
+ warn(`session ${values.resume} ran on ${ran}; the meta model is now ${wiring.model}, so this starts a new session`);
427
+ values = { ...values, resume: undefined };
428
+ }
429
+ // Named up front, so a run killed before copilot reports its id can still be resumed.
430
+ let session = values.resume ?? (values.continue ? undefined : (fresh ?? randomUUID()));
431
+ const args = argv(values, project.dir, prompt, wiring.secret, session);
432
+ const retries = count(values.retries, '--retries') ?? DEFAULT_RESUMES;
284
433
  if (values['dry-run']) {
285
434
  const lines = masked(wiring.env, wiring.secret);
286
435
  if (ctx.json) {
@@ -310,33 +459,112 @@ async function go(ctx, values, project, prompt) {
310
459
  note(dim('set another: zen meta model <provider>/<id>'));
311
460
  }
312
461
  note(cyan(`${owner} · ${wiring.model} · ${project.name}`));
313
- const log = openLog(project.dir);
462
+ const log = openLog(project.dir, new Date(), session);
314
463
  note(`log ${log.path}`);
315
464
  const elided = args.map((a, i) => (args[i - 1] === '-p' ? '<prompt>' : a));
316
465
  log.line(`zen meta ${new Date().toISOString()}`);
317
466
  log.line(`project ${project.name} ${project.dir}`);
318
467
  log.line(`model ${chosen.ref} from ${SOURCE_LABELS[chosen.from]}`);
319
468
  log.line(`command ${[binary.command, ...binary.args, ...elided].join(' ')}`);
469
+ if (session) {
470
+ recordSession(project.dir, session);
471
+ log.line(`session ${session}`);
472
+ }
473
+ const relay = wiring.relay ? await startVertexRelay(wiring.relay) : undefined;
474
+ if (relay) {
475
+ wiring.env.COPILOT_PROVIDER_BASE_URL = relay.url;
476
+ log.line(`relay ${relay.url} -> vertex rawPredict`);
477
+ }
320
478
  log.line('');
321
479
  log.line('--- prompt ---');
322
480
  log.line(prompt);
323
481
  log.line('');
324
482
  log.line('--- run ---');
483
+ const resumeHint = () => session ? `resume it: zen meta resume ${project.name} ${session}` : undefined;
484
+ // Every zen command the agent runs inherits these, so a batch it starts or
485
+ // an ask it makes lands in the same ledger, tagged with this session.
486
+ const spans = log.path.replace(/\.log$/, '.otel.jsonl');
487
+ const ledger = ledgerPath(project.dir);
488
+ const env = {
489
+ ...wiring.env,
490
+ COPILOT_OTEL_FILE_EXPORTER_PATH: spans,
491
+ ...(ledger ? { [LEDGER_ENV]: ledger } : {}),
492
+ ...(session ? { [META_SESSION_ENV]: session } : {}),
493
+ ...(promptName ? { [META_PROMPT_ENV]: promptName } : {}),
494
+ };
495
+ const tally = new Tally();
496
+ const record = values.events ? copilotEventLog(resolve(ctx.cwd, values.events)) : undefined;
497
+ const tail = tailSpans(spans, (call) => {
498
+ tally.add(call);
499
+ record?.call(call);
500
+ appendUsage(project.dir, {
501
+ kind: 'meta.call',
502
+ ts: new Date(Date.parse(call.startedAt) + call.durationMs).toISOString(),
503
+ ...call,
504
+ session: call.session ?? session,
505
+ });
506
+ });
507
+ const reporter = usageReporter(project.dir);
508
+ const tokens = () => {
509
+ tail.stop();
510
+ return tally.toString();
511
+ };
325
512
  // A run that dies still has to leave a readable file behind.
326
513
  try {
327
- const outcome = await launch({
328
- binary,
329
- args,
330
- env: wiring.env,
331
- cwd: project.dir,
332
- log,
333
- });
514
+ let resumes = 0;
515
+ const run = async (argList) => {
516
+ const launchedAt = new Date();
517
+ const outcome = await launch({
518
+ binary,
519
+ args: argList,
520
+ env,
521
+ cwd: project.dir,
522
+ log,
523
+ sink: ctx.json ? terminalSink : undefined,
524
+ onEvent: record?.event,
525
+ status: () => (tally.calls > 0 ? tally.toString() : ''),
526
+ });
527
+ if (outcome.sessionId && outcome.sessionId !== session) {
528
+ session = outcome.sessionId;
529
+ recordSession(project.dir, session);
530
+ }
531
+ const models = session ? sessionTotals(session) : undefined;
532
+ if (session && models) {
533
+ appendUsage(project.dir, {
534
+ kind: 'meta.session',
535
+ ts: new Date().toISOString(),
536
+ session,
537
+ models,
538
+ exitCode: outcome.exitCode,
539
+ resumes,
540
+ startedAt: launchedAt.toISOString(),
541
+ durationMs: Date.now() - launchedAt.getTime(),
542
+ });
543
+ }
544
+ return outcome;
545
+ };
546
+ let outcome = await run(args);
547
+ for (let reason = transient(outcome); reason && resumes < retries;) {
548
+ const wait = resumeDelayMs(resumes);
549
+ resumes += 1;
550
+ const said = `${reason}: resuming in ${duration(wait)} (${resumes}/${retries})`;
551
+ warn(said);
552
+ log.line('');
553
+ log.line(`--- resume ${resumes}: ${said} ---`);
554
+ await pause(wait, resumeHint());
555
+ const again = { ...values, resume: session, continue: false };
556
+ outcome = await run(argv(again, project.dir, RESUME_PROMPT, wiring.secret));
557
+ reason = transient(outcome);
558
+ }
334
559
  log.line('');
335
560
  log.line('--- answer ---');
336
561
  log.line(outcome.answer);
337
562
  if (outcome.answer) {
338
563
  log.saveAnswer(outcome.answer);
339
564
  }
565
+ const spent = tokens();
566
+ log.line('');
567
+ log.line(`tokens ${spent}`);
340
568
  if (ctx.json) {
341
569
  json({
342
570
  project: project.dir,
@@ -344,7 +572,9 @@ async function go(ctx, values, project, prompt) {
344
572
  model: chosen.ref,
345
573
  sessionId: outcome.sessionId,
346
574
  exitCode: outcome.exitCode,
575
+ resumes,
347
576
  usage: outcome.usage,
577
+ tokens: { calls: tally.calls, ...tally.usage },
348
578
  answer: outcome.answer,
349
579
  answerFile: outcome.answer ? log.answerPath : undefined,
350
580
  });
@@ -355,16 +585,82 @@ async function go(ctx, values, project, prompt) {
355
585
  // have read enough to want it in an editor.
356
586
  note(cyan(log.answerPath));
357
587
  }
588
+ if (!ctx.json && tally.calls > 0) {
589
+ note(dim(`tokens ${spent}`));
590
+ }
358
591
  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);
592
+ throw new CliError(`the meta agent exited ${outcome.exitCode}`, EXIT.failed, resumeHint() ??
593
+ (binary.from === 'npx' ? 'install it: npm i -g @github/copilot' : undefined));
594
+ }
595
+ }
596
+ catch (err) {
597
+ const hint = resumeHint();
598
+ if (hint && !(err instanceof CliError)) {
599
+ note(dim(hint));
360
600
  }
601
+ throw err;
361
602
  }
362
603
  finally {
604
+ tail.stop();
605
+ reporter.stop();
606
+ await relay?.close();
363
607
  log.close();
364
608
  }
365
609
  }
610
+ /** How often a running meta agent rewrites the fine-tuning's usage report. */
611
+ const REPORT_EVERY_MS = 5 * 60_000;
612
+ /**
613
+ * Rewrites `finetune/USAGE.md` on a timer and once more at the end, when the
614
+ * project is being tuned. Detached and silent: the report is a by-product, and
615
+ * neither its time nor its failure belongs to the run.
616
+ */
617
+ function usageReporter(dir) {
618
+ const script = join(dir, '.github/skills/zen-finetune/scripts/usage.mjs');
619
+ if (!existsSync(join(dir, 'finetune')) || !existsSync(script)) {
620
+ return { stop: () => undefined };
621
+ }
622
+ let running = false;
623
+ const refresh = () => {
624
+ if (running) {
625
+ return;
626
+ }
627
+ running = true;
628
+ const child = spawn(process.execPath, [script, '-q'], {
629
+ cwd: dir,
630
+ stdio: 'ignore',
631
+ detached: true,
632
+ });
633
+ child.on('exit', () => (running = false));
634
+ child.on('error', () => (running = false));
635
+ child.unref();
636
+ };
637
+ const timer = setInterval(refresh, REPORT_EVERY_MS);
638
+ timer.unref();
639
+ return {
640
+ stop: () => {
641
+ clearInterval(timer);
642
+ running = false;
643
+ refresh();
644
+ },
645
+ };
646
+ }
647
+ /** The wait before a resume, cut short by Ctrl-C - which still says how to resume. */
648
+ async function pause(ms, hint) {
649
+ const stop = new AbortController();
650
+ const onInt = () => stop.abort();
651
+ process.once('SIGINT', onInt);
652
+ try {
653
+ await sleep(ms, undefined, { signal: stop.signal });
654
+ }
655
+ catch {
656
+ throw new CliError('interrupted', EXIT.failed, hint);
657
+ }
658
+ finally {
659
+ process.off('SIGINT', onInt);
660
+ }
661
+ }
366
662
  /** Copilot's own flags, assembled once. Nothing secret goes on this line. */
367
- function argv(values, dir, prompt, secret) {
663
+ function argv(values, dir, prompt, secret, fresh) {
368
664
  const args = ['-C', dir, '-p', prompt, '--output-format', 'json', '--no-color'];
369
665
  // A prompt written for an editor expects to read, write and run things, and
370
666
  // there is nobody at a `-p` run to answer the question. Naming tools is the
@@ -390,9 +686,12 @@ function argv(values, dir, prompt, secret) {
390
686
  if (values.resume) {
391
687
  args.push('--resume', values.resume);
392
688
  }
393
- if (values.continue) {
689
+ else if (values.continue) {
394
690
  args.push('--continue');
395
691
  }
692
+ else if (fresh) {
693
+ args.push('--session-id', fresh);
694
+ }
396
695
  if (secret.length > 0) {
397
696
  args.push('--secret-env-vars', secret.join(','));
398
697
  }
@@ -417,6 +716,16 @@ function projectModel(dir) {
417
716
  return undefined;
418
717
  }
419
718
  }
719
+ function count(text, flag) {
720
+ if (text === undefined) {
721
+ return undefined;
722
+ }
723
+ const value = Number(text);
724
+ if (!Number.isInteger(value) || value < 0) {
725
+ throw usageError(`${flag} takes a whole number, 0 or more`, `got "${text}"`);
726
+ }
727
+ return value;
728
+ }
420
729
  function asProvider(name) {
421
730
  const owner = assertOwner(name);
422
731
  if (owner === 'exa') {
@@ -534,7 +843,7 @@ async function vet(ctx, ref) {
534
843
  // The colon form, so a refusal suggests `zen models ls <provider>`.
535
844
  ref: `${owner}:${id}`,
536
845
  kind: 'model',
537
- model: createModel({ provider: owner, model: id, maxTokens: 16 }),
846
+ model: probeChat(owner, id),
538
847
  });
539
848
  bar?.done();
540
849
  if (probe.check.state !== 'live') {