@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,14 +1,64 @@
1
1
  import type { Command } from '../command.ts';
2
- import { type NodeDetail } from '../trace.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
+ raw?: boolean;
15
+ open?: boolean;
16
+ rebuild?: boolean;
17
+ 'no-timing'?: boolean;
18
+ 'no-style'?: boolean;
19
+ }
3
20
  export declare const inspect: Command;
21
+ /** The `--part` names that match no part of any of these nodes. */
22
+ export declare function missingParts(found: readonly NodeDetail[], want: readonly string[]): string[];
4
23
  /**
5
24
  * Which parts print in full: everything `--part` names, or everything but
6
- * `request`. A prefix matches, so `--part call` catches `call run_command (…)`.
25
+ * the bulky ones. A prefix matches, so `--part call` catches `call run_command (…)`.
26
+ * A name that matches nothing is an error only when no name matches at all.
7
27
  */
8
28
  export declare function wholeParts(found: readonly NodeDetail[], values: {
9
29
  part?: string[];
10
30
  full?: boolean;
11
31
  }): (name: string) => boolean;
32
+ /**
33
+ * One recorded call, put back to a model with a question on the end.
34
+ *
35
+ * `node` shows what the model was given; this asks the model what it made of
36
+ * it. Everything it sees is what it saw at the time, so an answer naming the
37
+ * skill that steered it is checkable against the same node.
38
+ */
39
+ export declare function replayableCalls(trace: Trace): {
40
+ entry: TraceEntry;
41
+ node: import("@zenera/neo").LlmCallNode;
42
+ }[];
43
+ export declare function renderAskExchange(question: string, answer: string, columns?: number): string[];
44
+ export declare function repeatQuestions(initial: string, next: () => Promise<string>, answer: (question: string) => Promise<void>): Promise<void>;
45
+ /** The question: the words on the line, or the whole of `--question-file` (`-` is stdin). */
46
+ export declare function readQuestion(words: readonly string[], file: string | undefined, cwd: string): Promise<string>;
12
47
  /** One node, framed so a payload cannot be mistaken for the next node. */
13
48
  export declare function renderNode(node: NodeDetail, whole?: (name: string) => boolean): string;
49
+ export interface Located {
50
+ project: string;
51
+ session: SessionPaths;
52
+ run: RunPaths;
53
+ }
54
+ /**
55
+ * Which run, by whichever handle the caller has.
56
+ *
57
+ * A directory is the handle a *program* holds: `zen run --json` reports one,
58
+ * and asking it to be taken apart into a project, a session and a run before
59
+ * it can be used again would be work for nothing. A person holds an id, or
60
+ * nothing at all and gets asked.
61
+ */
62
+ export declare function locate(cwd: string, values: Pick<Flags, 'project' | 'session' | 'run' | 'dir'>, positional: string | undefined, asking: boolean): Promise<Located>;
63
+ export {};
14
64
  //# sourceMappingURL=inspect.d.ts.map
@@ -1,8 +1,9 @@
1
- import { assertState, buildRunReport, FilePayloadStore, memoryDir, MemoryStore, PayloadResolver, projectRegistry, readProjectConfig, renderReportHtml, } from '@zenera/neo';
1
+ import { assertState, buildRunReport, FilePayloadStore, memoryDir, MemoryStore, PayloadResolver, projectRegistry, readProjectConfig, renderReportHtml, SANDBOX_MOUNT, text, } from '@zenera/neo';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { readFile, writeFile } from 'node:fs/promises';
5
5
  import { join, resolve } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
6
7
  import { parse } from "../args.js";
7
8
  import { buildDiagnostic, parseRecordedRequest, pickModel } from "../ask.js";
8
9
  import { loadProjectEnv } from "../env.js";
@@ -11,7 +12,10 @@ import { sessionIds } from "../projects.js";
11
12
  import { project as resolveProject } from "../resolve.js";
12
13
  import { display, listRuns, listSessions, newestRun, readRunMeta, requireSession, runPaths, runPathsAt, sessionPaths, } from "../session.js";
13
14
  import { nodeDetail, parseNodeIds, traceIndex, traceMermaid, traceOf, } from "../trace.js";
14
- import { ago, bold, bytes, choose, cyan, dim, invalidError, isInteractive, json, note, usageError, write, writeAll, yellow, } from "../term.js";
15
+ import { formatMarkdown } from "../tui/markdown.js";
16
+ import { boxWidth } from "../tui/wrap.js";
17
+ import { ago, bold, bytes, choose, cyan, dim, invalidError, isInteractive, json, note, pad, ask as prompt, readStdin, usageError, write, writeAll, yellow, } from "../term.js";
18
+ import { appendUsage } from "../usage.js";
15
19
  const USAGE = 'zen inspect [report|graph|node|ask] [run] [--dir <run dir>] [--open]';
16
20
  const SUBCOMMANDS = ['report', 'graph', 'node', 'ask'];
17
21
  // ---------------------------------------------------------------------------
@@ -44,7 +48,8 @@ export const inspect = {
44
48
  ' report Build and print the path to report.html. Default.',
45
49
  ' graph The whole run as one Mermaid flowchart, on stdout.',
46
50
  ' node <id...> Those nodes of the graph in full. Ranges: n5..n9.',
47
- ' ask <id> <question> Put a question to the model that made that call.',
51
+ ' ask <id> <question> One answer on stdout, then exit. Never prompts.',
52
+ ' ask [<id>] A conversation at a terminal. Prompts for the rest.',
48
53
  '',
49
54
  ' --project <name|dir> Which project. Defaults to the one you are in.',
50
55
  ' --session <id> Which session. Defaults to the newest that ran.',
@@ -52,15 +57,17 @@ export const inspect = {
52
57
  ' --dir <dir> A run directory, as `zen run --json` reports it.',
53
58
  ' --memory <dir> Read this memory instead of the project’s.',
54
59
  ' --model <ref> Answer `ask` with this model instead of the run’s.',
60
+ ' --question-file <path> Read the `ask` question from a file; `-` is stdin.',
55
61
  ' --part <name> Print this part of a node in full. Repeatable.',
56
62
  ' --full Print every part, the recorded `request` included.',
63
+ ' --raw With one node and one --part: that part alone, unframed.',
57
64
  ' --rebuild Build report.html again from the run state.',
58
65
  ' --open Open the report in a browser.',
59
66
  ' --no-timing Leave the clock off the graph.',
60
67
  ' --no-style Leave the colours off the graph. Shorter to read.',
61
68
  '',
62
- 'With no arguments: asks which session and run, or takes the newest of',
63
- 'each when there is nothing to ask on.',
69
+ 'With no run named, report, graph and node ask which session and run at',
70
+ 'a terminal, and take the newest of each when there is nothing to ask on.',
64
71
  '',
65
72
  '`graph` is written to be read by a model. Nodes are declared in the',
66
73
  'order they happened, with ids `n1`, `n2`, …; every edge is collected in',
@@ -79,12 +86,38 @@ export const inspect = {
79
86
  '',
80
87
  ' zen inspect node n11 --part thinking --part text --dir <run dir>',
81
88
  '',
89
+ 'An `llm_call` also carries two parts read out of its request, printed',
90
+ 'only when named: `tools`, the tool names that call was offered, and',
91
+ '`messages`, the conversation it saw, one clipped line per message.',
92
+ '`--raw` prints one part with nothing around it, for a pipe:',
93
+ '',
94
+ ' zen inspect node n11 --part tools --dir <run dir>',
95
+ " zen inspect node n11 --part request --raw --dir <run dir> | jq '.messages'",
96
+ '',
82
97
  '`ask` replays one `llm_call` — its system prompt, its messages, its tool',
83
98
  'schemas — to a model, with your question on the end and tool calling off.',
84
99
  'It is told the run is over and that it may quote its own instructions, so',
85
- 'the answer names the prompt or skill behind the behaviour:',
100
+ 'the answer names the prompt or skill behind the behaviour. It has two',
101
+ 'modes, and which one is decided by whether the question is given:',
102
+ '',
103
+ 'One-shot — a question on the line, in --question-file, or --json. For a',
104
+ 'script or a model: no banner, no prompt, no picker; stdout is the answer',
105
+ 'and nothing else. Name the run with --dir, --run or --session, or the',
106
+ 'newest is taken.',
86
107
  '',
87
108
  ' zen inspect ask n11 "why run python -c when the skill says npm test?"',
109
+ ' zen inspect ask n11 --question-file q.md --dir <run dir>',
110
+ '',
111
+ 'A question with quotes, backticks or several lines goes in a file, so no',
112
+ 'shell ever parses it.',
113
+ '',
114
+ 'Interactive — no question. For a person at a terminal: asks for whatever',
115
+ 'is not named (session, run, LLM call), then the question, and keeps the',
116
+ 'conversation going until an empty question. Without a terminal it is an',
117
+ 'error, never a wait.',
118
+ '',
119
+ ' zen inspect ask',
120
+ ' zen inspect ask n11 --dir <run dir>',
88
121
  '',
89
122
  'All of it, at length: .github/skills/zen-cli/references/inspect.md',
90
123
  ],
@@ -97,7 +130,9 @@ export const inspect = {
97
130
  memory: { type: 'string' },
98
131
  model: { type: 'string' },
99
132
  part: { type: 'string', multiple: true },
133
+ 'question-file': { type: 'string' },
100
134
  full: { type: 'boolean' },
135
+ raw: { type: 'boolean' },
101
136
  open: { type: 'boolean' },
102
137
  rebuild: { type: 'boolean' },
103
138
  'no-timing': { type: 'boolean' },
@@ -117,8 +152,13 @@ export const inspect = {
117
152
  return await nodes(at, values, rest, ctx.json);
118
153
  }
119
154
  if (what === 'ask') {
120
- const at = await locate(ctx.cwd, values, undefined, asking);
121
- return await ask(at, values, rest, ctx.json);
155
+ // Given a question it is one-shot; only a bare `ask` converses.
156
+ const oneShot = ctx.json || values['question-file'] !== undefined || rest.length > 1;
157
+ if (!oneShot && !(asking && process.stdout.isTTY)) {
158
+ throw usageError('interactive `ask` needs a terminal; give the node id and a question', 'zen inspect ask n11 --question-file q.md --dir <run dir>');
159
+ }
160
+ const at = await locate(ctx.cwd, values, undefined, !oneShot);
161
+ return await ask(at, values, rest, ctx.cwd, ctx.json, !oneShot);
122
162
  }
123
163
  const at = await locate(ctx.cwd, values, rest[0], asking);
124
164
  if (what === 'graph') {
@@ -209,18 +249,42 @@ async function nodes(at, values, ids, asJson) {
209
249
  return;
210
250
  }
211
251
  const whole = wholeParts(found, values);
212
- writeAll(preamble(run.id, found, whole, trace.entries.length));
252
+ if (values.raw) {
253
+ const [node] = found;
254
+ const hits = node?.parts.filter((p) => whole(p.name)) ?? [];
255
+ if (found.length !== 1 || values.part?.length !== 1 || hits.length !== 1) {
256
+ throw usageError('--raw prints one part of one node', hits.length > 1
257
+ ? `name one: ${hits.map((p) => `--part ${flag(p.name)}`).join(', ')}`
258
+ : 'zen inspect node n11 --part request --raw --dir <run dir>');
259
+ }
260
+ const { text } = hits[0];
261
+ process.stdout.write(text.endsWith('\n') ? text : `${text}\n`);
262
+ return;
263
+ }
264
+ writeAll([
265
+ ...preamble(run.id, found, whole, trace.entries.length),
266
+ ...missingParts(found, values.part ?? []).map((name) => `# no part starts with "${name}" in these nodes`),
267
+ ]);
213
268
  for (const node of found) {
214
269
  write(renderNode(node, whole));
215
270
  }
216
- note(dim(`the rest of the run: ${cyan(`zen inspect graph --dir ${run.dir}`)}`));
217
- note(dim(`why it did this: ${cyan(`zen inspect ask ${found[0]?.id ?? 'n1'} "…" --dir ${run.dir}`)}`));
271
+ // A model reading this already has the commands; only a person needs reminding.
272
+ if (process.stderr.isTTY) {
273
+ note(dim(`the rest of the run: ${cyan(`zen inspect graph --dir ${run.dir}`)}`));
274
+ note(dim(`why it did this: ${cyan(`zen inspect ask ${found[0]?.id ?? 'n1'} "…" --dir ${run.dir}`)}`));
275
+ }
276
+ }
277
+ /** Parts `node` names with their size instead of printing, unless asked for. */
278
+ const BULKY = new Set(['request', 'messages', 'tools']);
279
+ /** The `--part` names that match no part of any of these nodes. */
280
+ export function missingParts(found, want) {
281
+ const names = found.flatMap((n) => n.parts.map((p) => p.name));
282
+ return want.filter((asked) => !names.some((name) => name.startsWith(asked)));
218
283
  }
219
- /** The part `--part` leaves out unless it is asked for by name. */
220
- const BULKY = 'request';
221
284
  /**
222
285
  * Which parts print in full: everything `--part` names, or everything but
223
- * `request`. A prefix matches, so `--part call` catches `call run_command (…)`.
286
+ * the bulky ones. A prefix matches, so `--part call` catches `call run_command (…)`.
287
+ * A name that matches nothing is an error only when no name matches at all.
224
288
  */
225
289
  export function wholeParts(found, values) {
226
290
  if (values.full) {
@@ -228,13 +292,12 @@ export function wholeParts(found, values) {
228
292
  }
229
293
  const want = values.part ?? [];
230
294
  if (!want.length) {
231
- return (name) => name !== BULKY;
295
+ return (name) => !BULKY.has(name);
232
296
  }
233
- const names = [...new Set(found.flatMap((n) => n.parts.map((p) => p.name)))];
234
- for (const asked of want) {
235
- if (!names.some((name) => name.startsWith(asked))) {
236
- throw usageError(`no part starts with "${asked}" in ${found.length === 1 ? found[0]?.id : 'these nodes'}`, names.length ? `they carry: ${names.join(', ')}` : 'they carry no payloads');
237
- }
297
+ const missing = missingParts(found, want);
298
+ if (missing.length === want.length) {
299
+ const names = [...new Set(found.flatMap((n) => n.parts.map((p) => p.name)))];
300
+ throw usageError(`no part starts with "${want[0]}" in ${found.length === 1 ? found[0]?.id : 'these nodes'}`, names.length ? `they carry: ${names.join(', ')}` : 'they carry no payloads');
238
301
  }
239
302
  return (name) => want.some((asked) => name.startsWith(asked));
240
303
  }
@@ -245,14 +308,82 @@ export function wholeParts(found, values) {
245
308
  * it. Everything it sees is what it saw at the time, so an answer naming the
246
309
  * skill that steered it is checkable against the same node.
247
310
  */
248
- async function ask(at, values, rest, asJson) {
311
+ export function replayableCalls(trace) {
312
+ return trace.entries.flatMap((entry) => entry.node.type === 'llm_call' && entry.node.request ? [{ entry, node: entry.node }] : []);
313
+ }
314
+ const ASK_BOX = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' };
315
+ function askPanel(title, text, outer, border) {
316
+ const inner = outer - 4;
317
+ const rule = ASK_BOX.h.repeat(Math.max(1, outer - title.length - 5));
318
+ return [
319
+ border(`${ASK_BOX.tl}${ASK_BOX.h} ${title} ${rule}${ASK_BOX.tr}`),
320
+ ...formatMarkdown(text, inner).map((line) => `${border(ASK_BOX.v)} ${pad(line, inner)} ${border(ASK_BOX.v)}`),
321
+ border(`${ASK_BOX.bl}${ASK_BOX.h.repeat(outer - 2)}${ASK_BOX.br}`),
322
+ ];
323
+ }
324
+ export function renderAskExchange(question, answer, columns = 80) {
325
+ const outer = boxWidth(`${question}\n\n${answer}`, columns);
326
+ return [
327
+ '',
328
+ ...askPanel('Question', question, outer, cyan),
329
+ '',
330
+ ...askPanel('Answer', answer, outer, dim),
331
+ '',
332
+ ];
333
+ }
334
+ export async function repeatQuestions(initial, next, answer) {
335
+ let question = initial;
336
+ while (question) {
337
+ await answer(question);
338
+ question = (await next()).trim();
339
+ }
340
+ }
341
+ /** The question: the words on the line, or the whole of `--question-file` (`-` is stdin). */
342
+ export async function readQuestion(words, file, cwd) {
343
+ const typed = words.join(' ').trim();
344
+ if (file === undefined) {
345
+ return typed;
346
+ }
347
+ if (typed) {
348
+ throw usageError('give the question either as words or as --question-file, not both', 'zen inspect ask n11 --question-file q.md');
349
+ }
350
+ if (file === '-') {
351
+ return (await readStdin()) ?? '';
352
+ }
353
+ try {
354
+ return (await readFile(resolve(cwd, file), 'utf8')).trim();
355
+ }
356
+ catch (err) {
357
+ throw invalidError(`cannot read question file ${file}: ${err.code ?? err}`, 'write the question to that path first');
358
+ }
359
+ }
360
+ async function ask(at, values, rest, cwd, asJson, interactive) {
249
361
  const { project, session, run } = at;
250
- const [id, ...words] = rest;
251
- const query = words.join(' ').trim();
252
- if (!id || !query) {
362
+ const trace = traceOf(await readState(run));
363
+ let [id, ...words] = rest;
364
+ if (!id && interactive) {
365
+ const calls = replayableCalls(trace);
366
+ if (calls.length === 0) {
367
+ throw invalidError(`run ${run.id} has no recorded LLM call to replay`, 'only runs that record requests can be asked about');
368
+ }
369
+ const entry = await choose('Which LLM call?', calls.map((candidate) => ({
370
+ label: `${candidate.entry.key} ${candidate.node.model}`,
371
+ detail: [
372
+ candidate.node.agent,
373
+ candidate.node.toolCalls.length
374
+ ? `calls ${candidate.node.toolCalls.map((call) => call.name).join(', ')}`
375
+ : '',
376
+ candidate.entry.branch ? `branch ${candidate.entry.branch}` : '',
377
+ ]
378
+ .filter(Boolean)
379
+ .join(' '),
380
+ value: candidate.entry,
381
+ })));
382
+ id = entry.key;
383
+ }
384
+ if (!id) {
253
385
  throw usageError('ask takes one node id and a question', 'zen inspect ask n11 "why did you run python -c instead of the tests?"');
254
386
  }
255
- const trace = traceOf(await readState(run));
256
387
  let wanted;
257
388
  try {
258
389
  wanted = parseNodeIds([id], trace.byKey);
@@ -272,6 +403,15 @@ async function ask(at, values, rest, asJson) {
272
403
  throw invalidError(`${entry.key} did not record the request that produced it`, 'only a run made by this CLI records requests; an SDK run needs ' +
273
404
  '`runner({ recordRequests: true })`');
274
405
  }
406
+ const query = interactive
407
+ ? await prompt('Question (empty to finish)?')
408
+ : await readQuestion(words, values['question-file'], cwd);
409
+ if (!query && interactive) {
410
+ return;
411
+ }
412
+ if (!query) {
413
+ throw usageError('ask takes one node id and a question', 'zen inspect ask n11 "why did you run python -c instead of the tests?"');
414
+ }
275
415
  const payloads = new PayloadResolver(new FilePayloadStore({ dir: session.blobs, id: 'file' }));
276
416
  // Deliberately not the preview fallback `node` uses: a truncated request is
277
417
  // not the call that happened, and an answer about it would be about nothing.
@@ -292,8 +432,32 @@ async function ask(at, values, rest, asJson) {
292
432
  'a second opinion, not the model looking at itself'));
293
433
  }
294
434
  const model = projectRegistry(config).model(picked.ref);
295
- const res = await model.generate(buildDiagnostic({ request: recorded, answer, toolCalls, query }));
435
+ const record = async (call) => {
436
+ const startedAt = new Date();
437
+ const res = await call();
438
+ appendUsage(project, {
439
+ kind: 'ask',
440
+ ts: new Date().toISOString(),
441
+ runDir: run.dir,
442
+ node: entry.key,
443
+ nodeAgent: node.agent,
444
+ nodeModel: node.model,
445
+ model: picked.label,
446
+ usage: res.usage,
447
+ startedAt: startedAt.toISOString(),
448
+ durationMs: Date.now() - startedAt.getTime(),
449
+ });
450
+ return res;
451
+ };
452
+ const generate = (question) => record(() => model.generate(buildDiagnostic({ request: recorded, answer, toolCalls, query: question })));
453
+ const usage = (res) => {
454
+ note(dim(`${bold(entry.key)} ${picked.label}` +
455
+ (res.usage
456
+ ? ` · ${res.usage.inputTokens} in, ${res.usage.outputTokens} out`
457
+ : '')));
458
+ };
296
459
  if (asJson) {
460
+ const res = await generate(query);
297
461
  json({
298
462
  session: session.id,
299
463
  run: run.id,
@@ -306,9 +470,28 @@ async function ask(at, values, rest, asJson) {
306
470
  });
307
471
  return;
308
472
  }
309
- write(res.text);
310
- note(dim(`${bold(entry.key)} ${picked.label}` +
311
- (res.usage ? ` · ${res.usage.inputTokens} in, ${res.usage.outputTokens} out` : '')));
473
+ if (!interactive) {
474
+ write((await generate(query)).text);
475
+ return;
476
+ }
477
+ let conversation;
478
+ await repeatQuestions(query, () => prompt('Question (empty to finish)?'), async (question) => {
479
+ if (!conversation) {
480
+ conversation = buildDiagnostic({
481
+ request: recorded,
482
+ answer,
483
+ toolCalls,
484
+ query: question,
485
+ });
486
+ }
487
+ else {
488
+ conversation.messages.push({ role: 'user', content: [text(question)] });
489
+ }
490
+ const res = await record(() => model.generate(conversation));
491
+ conversation.messages.push({ role: 'assistant', content: res.text });
492
+ writeAll(renderAskExchange(question, res.text, process.stdout.columns ?? 80));
493
+ usage(res);
494
+ });
312
495
  }
313
496
  /**
314
497
  * A part name as it can be typed back. The tool-call parts are named
@@ -371,7 +554,7 @@ export function renderNode(node, whole = () => true) {
371
554
  * it can be used again would be work for nothing. A person holds an id, or
372
555
  * nothing at all and gets asked.
373
556
  */
374
- async function locate(cwd, values, positional, asking) {
557
+ export async function locate(cwd, values, positional, asking) {
375
558
  // A run id is a stamp and never has a separator in it, so a positional
376
559
  // with one is unambiguously a path.
377
560
  const asPath = positional?.includes('/') ? positional : undefined;
@@ -469,7 +652,13 @@ async function pickRun(session, asked, asking) {
469
652
  async function rebuild(session, run, store) {
470
653
  const state = await readState(run);
471
654
  const payloads = new PayloadResolver(new FilePayloadStore({ dir: session.blobs, id: 'file' }));
472
- const report = await buildRunReport(state, payloads, { title: run.id, memory: store });
655
+ const { workspace } = await readRunMeta(run);
656
+ const report = await buildRunReport(state, payloads, {
657
+ title: run.id,
658
+ memory: store,
659
+ // The run never recorded a `workdir:`, so the default name is the guess.
660
+ mounts: workspace ? [{ at: SANDBOX_MOUNT, href: pathToFileURL(workspace).href }] : [],
661
+ });
473
662
  await writeFile(run.report, renderReportHtml(report), 'utf8');
474
663
  }
475
664
  /**