@nseng-ai/vibechk 0.1.1

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.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@nseng-ai/vibechk",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "files": [
6
+ "src"
7
+ ],
8
+ "engines": {
9
+ "node": ">=24.12.0",
10
+ "pnpm": ">=11.8.0"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "bin": {
16
+ "vibechk": "src/cli.ts"
17
+ },
18
+ "exports": {
19
+ ".": "./src/index.ts"
20
+ },
21
+ "dependencies": {
22
+ "@nseng-ai/capability-kit": "0.1.1",
23
+ "@nseng-ai/clinkr": "0.1.1",
24
+ "@nseng-ai/foundation": "0.1.1",
25
+ "zod": "^4.4.3"
26
+ }
27
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ ClinkrGroup,
5
+ failure,
6
+ negative,
7
+ ok,
8
+ usageError,
9
+ type ClinkrExit,
10
+ type ClinkrFailureExit,
11
+ type ClinkrNegativeExit,
12
+ type ClinkrUsageErrorExit,
13
+ type RenderCapabilities,
14
+ } from "@nseng-ai/clinkr";
15
+ import { rawCommand } from "@nseng-ai/clinkr/raw";
16
+ import { defineCli, type CliEntrypointDeps } from "@nseng-ai/foundation/cli-runtime";
17
+ import { z } from "zod";
18
+
19
+ import type { ArtifactOutputBounds, LoadedBundle } from "./models.ts";
20
+ import {
21
+ renderComparisonReport,
22
+ renderRunReport,
23
+ renderRunsTable,
24
+ runListEntryToJson,
25
+ } from "./reports.ts";
26
+ import { listBundles, readBundle, resolveStoreRoot, VibechkError } from "./store.ts";
27
+ import type { VibechkWorkdirGateway } from "./repository.ts";
28
+ import { RealVibechkWorkdirGateway } from "./repository.ts";
29
+ import { buildProductionRunnerRegistry, type RunnerRegistry } from "./runners.ts";
30
+ import { generateRunId } from "./ids.ts";
31
+ import { executeRun } from "./workflow.ts";
32
+ export interface CliDeps extends CliEntrypointDeps {
33
+ runnerRegistry?: RunnerRegistry;
34
+ repositoryGatewayFactory?: (workdir: string) => VibechkWorkdirGateway;
35
+ clock?: () => Date;
36
+ idGenerator?: () => string;
37
+ defaultRunnerName?: string;
38
+ }
39
+
40
+ interface VibechkCliContext {
41
+ cwd: string;
42
+ env: NodeJS.ProcessEnv;
43
+ runnerRegistry: RunnerRegistry;
44
+ repositoryGatewayFactory: (workdir: string) => VibechkWorkdirGateway;
45
+ clock: () => Date;
46
+ idGenerator: () => string;
47
+ defaultRunnerName: string;
48
+ stdout: (text: string) => void;
49
+ stderr: (text: string) => void;
50
+ }
51
+
52
+ const DEFAULT_RUNS_LIMIT = 50;
53
+ const DEFAULT_ARTIFACT_BYTE_LIMIT = 200_000;
54
+
55
+ const runsRequestSchema = z.object({
56
+ store: z.string().optional(),
57
+ outputFormat: z.enum(["table", "json"]).default("table"),
58
+ maxRuns: z.number().int().positive().default(DEFAULT_RUNS_LIMIT),
59
+ });
60
+
61
+ const showRequestSchema = z.object({
62
+ idOrPrefix: z.string(),
63
+ store: z.string().optional(),
64
+ maxArtifactBytes: z.number().int().positive().default(DEFAULT_ARTIFACT_BYTE_LIMIT),
65
+ });
66
+
67
+ const diffRequestSchema = z.object({
68
+ baselineId: z.string(),
69
+ treatmentId: z.string(),
70
+ store: z.string().optional(),
71
+ maxArtifactBytes: z.number().int().positive().default(DEFAULT_ARTIFACT_BYTE_LIMIT),
72
+ });
73
+
74
+ const runRequestSchema = z.object({
75
+ plan: z.string(),
76
+ workdir: z.string().default("."),
77
+ runner: z.string().optional(),
78
+ model: z.string().optional(),
79
+ store: z.string().optional(),
80
+ });
81
+
82
+ const entry = defineCli<VibechkCliContext, CliDeps, undefined>({
83
+ metaUrl: import.meta.url,
84
+ runtime: "typescript",
85
+ description: "Run lightweight agent context evals and publish Markdown evidence.",
86
+ prepareRun: ({ args, deps, cwd, env, stdout, stderr }) => {
87
+ const runnerRegistry = deps.runnerRegistry ?? buildProductionRunnerRegistry();
88
+ const repositoryGatewayFactory =
89
+ deps.repositoryGatewayFactory ??
90
+ ((workdir: string) => new RealVibechkWorkdirGateway({ workdir }));
91
+ const clock = deps.clock ?? (() => new Date());
92
+ const idGenerator = deps.idGenerator ?? generateRunId;
93
+ const defaultRunnerName = deps.defaultRunnerName ?? "claude";
94
+ const context: VibechkCliContext = {
95
+ cwd,
96
+ env,
97
+ runnerRegistry,
98
+ repositoryGatewayFactory,
99
+ clock,
100
+ idGenerator,
101
+ defaultRunnerName,
102
+ stdout: deps.stdout ?? stdout,
103
+ stderr: deps.stderr ?? stderr,
104
+ };
105
+ return {
106
+ type: "run",
107
+ context,
108
+ buildState: undefined,
109
+ args: normalizeRunsFormatArgs(args),
110
+ };
111
+ },
112
+ configureCli: ({ root }) => {
113
+ root.command({
114
+ name: "runs",
115
+ description: "List local vibechk run bundles from the configured store.",
116
+ schema: runsRequestSchema,
117
+ handler: runRuns,
118
+ renderHuman: renderRuns,
119
+ });
120
+
121
+ root.command({
122
+ name: "show",
123
+ description: "Render a Markdown report for a single run.",
124
+ schema: showRequestSchema,
125
+ positionals: {
126
+ idOrPrefix: { position: 0 },
127
+ },
128
+ handler: runShow,
129
+ renderHuman: renderShow,
130
+ });
131
+
132
+ root.command({
133
+ name: "diff",
134
+ description: "Render a Markdown comparison report for two runs.",
135
+ schema: diffRequestSchema,
136
+ positionals: {
137
+ baselineId: { position: 0 },
138
+ treatmentId: { position: 1 },
139
+ },
140
+ handler: runDiff,
141
+ renderHuman: renderDiff,
142
+ });
143
+
144
+ root.command(
145
+ rawCommand({
146
+ name: "run",
147
+ description: "Run a plan in a clean git workdir and persist a local run bundle.",
148
+ schema: runRequestSchema,
149
+ run: runRun,
150
+ }),
151
+ );
152
+ },
153
+ handleRunError: ({ error, stderr }) => {
154
+ if (error instanceof VibechkError) {
155
+ stderr(`Error: ${error.message}\n`);
156
+ return 1;
157
+ }
158
+ },
159
+ });
160
+
161
+ export const VERSION = entry.version;
162
+
163
+ export function buildCli(): ClinkrGroup<VibechkCliContext> {
164
+ return entry.buildCli(undefined);
165
+ }
166
+
167
+ type RunsRequest = z.infer<typeof runsRequestSchema>;
168
+ type ShowRequest = z.infer<typeof showRequestSchema>;
169
+ type DiffRequest = z.infer<typeof diffRequestSchema>;
170
+ type RunRequest = z.infer<typeof runRequestSchema>;
171
+
172
+ interface RunsOutputBounds {
173
+ kind: "runs-list";
174
+ appliedLimit: number;
175
+ returnedCount: number;
176
+ totalCount: number;
177
+ isComplete: boolean;
178
+ continuation: string | null;
179
+ }
180
+
181
+ type BoundedBundle = Omit<LoadedBundle, "planText" | "transcript" | "diffPatch"> & {
182
+ planText: string;
183
+ transcript: string;
184
+ diffPatch: string;
185
+ outputBounds: {
186
+ plan: ArtifactOutputBounds;
187
+ transcript: ArtifactOutputBounds;
188
+ diff: ArtifactOutputBounds;
189
+ };
190
+ };
191
+
192
+ type RunsResult =
193
+ | { type: "json"; entries: unknown[]; outputBounds: RunsOutputBounds }
194
+ | { type: "table"; loaded: LoadedBundle[]; outputBounds: RunsOutputBounds };
195
+ type VibechkReadOnlyErrorData = {
196
+ type: "lookup-error";
197
+ idOrPrefix?: unknown;
198
+ matches?: unknown;
199
+ };
200
+ type ShowResult = { loaded: BoundedBundle } | VibechkReadOnlyErrorData;
201
+ type DiffResult = { baseline: BoundedBundle; treatment: BoundedBundle } | VibechkReadOnlyErrorData;
202
+
203
+ async function runRuns(ctx: VibechkCliContext, request: RunsRequest) {
204
+ const storeRoot = resolveStoreRoot(request.store, ctx.env);
205
+ const loaded = await listBundles(storeRoot);
206
+ const bounded = loaded.slice(0, request.maxRuns);
207
+ const outputBounds = runsOutputBounds({
208
+ returnedCount: bounded.length,
209
+ totalCount: loaded.length,
210
+ appliedLimit: request.maxRuns,
211
+ });
212
+
213
+ if (request.outputFormat === "json") {
214
+ return ok<RunsResult>({
215
+ type: "json",
216
+ entries: bounded.map(runListEntryToJson),
217
+ outputBounds,
218
+ });
219
+ }
220
+ return ok<RunsResult>({ type: "table", loaded: bounded, outputBounds });
221
+ }
222
+
223
+ function renderRuns(result: RunsResult, caps: RenderCapabilities = { canEmitAnsi: false }): string {
224
+ if (result.type === "json") {
225
+ return JSON.stringify({ entries: result.entries, outputBounds: result.outputBounds });
226
+ }
227
+ if (result.loaded.length === 0) {
228
+ return "No vibechk runs found.";
229
+ }
230
+ const table = renderRunsTable(result.loaded, caps);
231
+ if (result.outputBounds.isComplete) return table;
232
+ return `${table}\n${result.outputBounds.continuation ?? "More runs are available."}`;
233
+ }
234
+
235
+ async function runShow(
236
+ ctx: VibechkCliContext,
237
+ request: ShowRequest,
238
+ ): Promise<ClinkrExit<ShowResult>> {
239
+ try {
240
+ const storeRoot = resolveStoreRoot(request.store, ctx.env);
241
+ const loaded = await readBundle(storeRoot, request.idOrPrefix);
242
+ return ok<ShowResult>({ loaded: boundBundleArtifacts(loaded, request.maxArtifactBytes) });
243
+ } catch (error) {
244
+ return vibechkReadOnlyErrorExit(error);
245
+ }
246
+ }
247
+
248
+ function renderShow(
249
+ result: ShowResult,
250
+ _caps: RenderCapabilities = { canEmitAnsi: false },
251
+ ): string {
252
+ if (!("loaded" in result)) return result.type;
253
+ return renderRunReport(result.loaded);
254
+ }
255
+
256
+ async function runDiff(
257
+ ctx: VibechkCliContext,
258
+ request: DiffRequest,
259
+ ): Promise<ClinkrExit<DiffResult>> {
260
+ try {
261
+ const storeRoot = resolveStoreRoot(request.store, ctx.env);
262
+ const baseline = await readBundle(storeRoot, request.baselineId);
263
+ const treatment = await readBundle(storeRoot, request.treatmentId);
264
+ return ok<DiffResult>({
265
+ baseline: boundBundleArtifacts(baseline, request.maxArtifactBytes),
266
+ treatment: boundBundleArtifacts(treatment, request.maxArtifactBytes),
267
+ });
268
+ } catch (error) {
269
+ return vibechkReadOnlyErrorExit(error);
270
+ }
271
+ }
272
+
273
+ function renderDiff(
274
+ result: DiffResult,
275
+ _caps: RenderCapabilities = { canEmitAnsi: false },
276
+ ): string {
277
+ if (!("baseline" in result)) return result.type;
278
+ return renderComparisonReport(result.baseline, result.treatment);
279
+ }
280
+
281
+ function runsOutputBounds(options: {
282
+ readonly returnedCount: number;
283
+ readonly totalCount: number;
284
+ readonly appliedLimit: number;
285
+ }): RunsOutputBounds {
286
+ const isComplete = options.returnedCount >= options.totalCount;
287
+ return {
288
+ kind: "runs-list",
289
+ appliedLimit: options.appliedLimit,
290
+ returnedCount: options.returnedCount,
291
+ totalCount: options.totalCount,
292
+ isComplete,
293
+ continuation: isComplete
294
+ ? null
295
+ : `Showing ${options.returnedCount} of ${options.totalCount} runs. Re-run with --max-runs ${options.totalCount} or use a narrower store.`,
296
+ };
297
+ }
298
+
299
+ function boundBundleArtifacts(loaded: LoadedBundle, maxArtifactBytes: number): BoundedBundle {
300
+ const plan = boundArtifact(loaded.planText, "plan", maxArtifactBytes);
301
+ const transcript = boundArtifact(loaded.transcript, "transcript", maxArtifactBytes);
302
+ const diff = boundArtifact(loaded.diffPatch, "diff", maxArtifactBytes);
303
+ return {
304
+ ...loaded,
305
+ planText: plan.text,
306
+ transcript: transcript.text,
307
+ diffPatch: diff.text,
308
+ outputBounds: {
309
+ plan: plan.outputBounds,
310
+ transcript: transcript.outputBounds,
311
+ diff: diff.outputBounds,
312
+ },
313
+ };
314
+ }
315
+
316
+ function boundArtifact(
317
+ text: string,
318
+ artifact: ArtifactOutputBounds["artifact"],
319
+ maxArtifactBytes: number,
320
+ ): { text: string; outputBounds: ArtifactOutputBounds } {
321
+ const originalBytes = Buffer.byteLength(text, "utf-8");
322
+ if (originalBytes <= maxArtifactBytes) {
323
+ return {
324
+ text,
325
+ outputBounds: {
326
+ kind: "artifact",
327
+ artifact,
328
+ appliedByteLimit: maxArtifactBytes,
329
+ originalBytes,
330
+ returnedBytes: originalBytes,
331
+ isComplete: true,
332
+ continuation: null,
333
+ },
334
+ };
335
+ }
336
+
337
+ const boundedText = textByByteLimit(text, maxArtifactBytes);
338
+ return {
339
+ text: boundedText,
340
+ outputBounds: {
341
+ kind: "artifact",
342
+ artifact,
343
+ appliedByteLimit: maxArtifactBytes,
344
+ originalBytes,
345
+ returnedBytes: Buffer.byteLength(boundedText, "utf-8"),
346
+ isComplete: false,
347
+ continuation: `${artifact} was truncated to ${maxArtifactBytes} bytes. Re-run with --max-artifact-bytes ${originalBytes} or inspect the run directory directly.`,
348
+ },
349
+ };
350
+ }
351
+
352
+ function textByByteLimit(text: string, maxBytes: number): string {
353
+ let bytes = 0;
354
+ let endIndex = 0;
355
+ for (const character of text) {
356
+ const nextBytes = bytes + Buffer.byteLength(character, "utf-8");
357
+ if (nextBytes > maxBytes) break;
358
+ bytes = nextBytes;
359
+ endIndex += character.length;
360
+ }
361
+ return text.slice(0, endIndex);
362
+ }
363
+
364
+ function vibechkReadOnlyErrorExit(
365
+ error: unknown,
366
+ ): ClinkrNegativeExit<VibechkReadOnlyErrorData> | ClinkrFailureExit | ClinkrUsageErrorExit {
367
+ if (!(error instanceof VibechkError)) throw error;
368
+ if (error.code === "run_not_found" || error.code === "ambiguous_run") {
369
+ return negative(error.message, { data: vibechkReadOnlyErrorData(error.data) });
370
+ }
371
+ if (error.code === "store_config_error") return usageError(error.message, error.data);
372
+ return failure(error.code, error.message, error.data);
373
+ }
374
+
375
+ function vibechkReadOnlyErrorData(data: Record<string, unknown>): VibechkReadOnlyErrorData {
376
+ return {
377
+ type: "lookup-error",
378
+ ...(data["idOrPrefix"] === undefined ? {} : { idOrPrefix: data["idOrPrefix"] }),
379
+ ...(data["matches"] === undefined ? {} : { matches: data["matches"] }),
380
+ };
381
+ }
382
+
383
+ async function runRun(ctx: VibechkCliContext, request: RunRequest): Promise<number> {
384
+ const runnerName = request.runner ?? ctx.defaultRunnerName;
385
+ const runner = ctx.runnerRegistry.get(runnerName);
386
+ const repository = ctx.repositoryGatewayFactory(request.workdir);
387
+
388
+ const result = await executeRun({
389
+ planPath: request.plan,
390
+ workdir: request.workdir,
391
+ runnerName,
392
+ model: request.model ?? null,
393
+ store: request.store,
394
+ env: ctx.env,
395
+ deps: {
396
+ runner,
397
+ repository,
398
+ clock: ctx.clock,
399
+ idGenerator: ctx.idGenerator,
400
+ stdout: ctx.stdout,
401
+ stderr: ctx.stderr,
402
+ },
403
+ });
404
+
405
+ ctx.stdout(`Run ID: ${result.runId}\n`);
406
+ return result.exitCode;
407
+ }
408
+
409
+ export async function runCli(args: readonly string[], deps: CliDeps = {}): Promise<number> {
410
+ return await entry.run(args, deps);
411
+ }
412
+
413
+ function normalizeRunsFormatArgs(args: readonly string[]): readonly string[] {
414
+ if (args[0] !== "runs") return args;
415
+ // `runs` exposes a domain outputFormat choice (`table|json`) to avoid colliding with
416
+ // Clinkr's global `--format human|json|markdown`; keep the old `runs --format` alias.
417
+ const normalized: string[] = [];
418
+ for (let index = 0; index < args.length; index += 1) {
419
+ const arg = args[index];
420
+ if (arg === undefined) continue;
421
+ if (arg === "--format") {
422
+ normalized.push("--output-format");
423
+ continue;
424
+ }
425
+ if (arg.startsWith("--format=")) {
426
+ normalized.push(`--output-format=${arg.slice("--format=".length)}`);
427
+ continue;
428
+ }
429
+ normalized.push(arg);
430
+ }
431
+ return normalized;
432
+ }
433
+
434
+ await entry.runIfMain({ isImportMetaMain: import.meta.main });
@@ -0,0 +1,37 @@
1
+ import type { CommandExecApi, ExecOptions, ExecResult } from "@nseng-ai/foundation/exec";
2
+
3
+ import { VibechkError } from "./store.ts";
4
+
5
+ export interface RunVibechkCommandOptions {
6
+ readonly execApi: CommandExecApi;
7
+ readonly command: string;
8
+ readonly args: readonly string[];
9
+ readonly execOptions?: ExecOptions;
10
+ readonly missingExecutableMessage: string;
11
+ readonly startupFailurePrefix: string;
12
+ }
13
+
14
+ export async function runVibechkCommand(options: RunVibechkCommandOptions): Promise<ExecResult> {
15
+ let result: ExecResult;
16
+ try {
17
+ result = await options.execApi.exec(options.command, [...options.args], options.execOptions);
18
+ } catch (error: unknown) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ throw commandStartupError(options, message);
21
+ }
22
+ if (result.startupError !== undefined) {
23
+ throw commandStartupError(options, result.startupError);
24
+ }
25
+ return result;
26
+ }
27
+
28
+ export function isMissingExecutableError(message: string): boolean {
29
+ return message.includes("ENOENT");
30
+ }
31
+
32
+ function commandStartupError(options: RunVibechkCommandOptions, message: string): VibechkError {
33
+ if (isMissingExecutableError(message)) {
34
+ return new VibechkError(options.missingExecutableMessage);
35
+ }
36
+ return new VibechkError(`${options.startupFailurePrefix}: ${message}`);
37
+ }
package/src/ids.ts ADDED
@@ -0,0 +1,12 @@
1
+ export function generateRunId(): string {
2
+ const hex = randomHex(4);
3
+ return hex.toLowerCase();
4
+ }
5
+
6
+ function randomHex(bytes: number): string {
7
+ const array = new Uint8Array(bytes);
8
+ crypto.getRandomValues(array);
9
+ return Array.from(array)
10
+ .map((byte) => byte.toString(16).padStart(2, "0"))
11
+ .join("");
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { buildCli, runCli, VERSION } from "./cli.ts";
2
+ export type { CliDeps } from "./cli.ts";
3
+ export type { GitProvenance, LoadedBundle, Metrics, RunBundle, RunStatus } from "./models.ts";
4
+ export { parseRunBundle } from "./models.ts";
5
+ export {
6
+ renderComparisonReport,
7
+ renderRunReport,
8
+ renderRunsTable,
9
+ runListEntryToJson,
10
+ } from "./reports.ts";
11
+ export { listBundles, readBundle, resolveStoreRoot, VibechkError } from "./store.ts";
package/src/models.ts ADDED
@@ -0,0 +1,161 @@
1
+ import { z } from "zod";
2
+
3
+ export type RunStatus = "success" | "failed";
4
+
5
+ export interface Metrics {
6
+ wallTimeSeconds: number | null;
7
+ inputTokens: number | null;
8
+ outputTokens: number | null;
9
+ totalTokens: number | null;
10
+ costUsd: number | null;
11
+ }
12
+
13
+ export interface GitProvenance {
14
+ repoRoot: string;
15
+ startingBranch: string;
16
+ startingCommit: string;
17
+ remotes: Record<string, string>;
18
+ }
19
+
20
+ export interface RunBundle {
21
+ schemaVersion: number;
22
+ runId: string;
23
+ status: RunStatus;
24
+ startedAt: Date;
25
+ finishedAt: Date;
26
+ runner: string;
27
+ runnerVersion: string | null;
28
+ model: string | null;
29
+ planSource: string;
30
+ workdir: string;
31
+ git: GitProvenance;
32
+ metrics: Metrics;
33
+ resultBranch: string | null;
34
+ branchCreated: boolean;
35
+ runnerExitCode: number;
36
+ error: string | null;
37
+ }
38
+
39
+ export interface LoadedBundle {
40
+ bundle: RunBundle;
41
+ runDir: string;
42
+ planText: string;
43
+ transcript: string;
44
+ diffPatch: string;
45
+ }
46
+
47
+ export interface ArtifactOutputBounds {
48
+ kind: "artifact";
49
+ artifact: "plan" | "transcript" | "diff";
50
+ appliedByteLimit: number;
51
+ originalBytes: number;
52
+ returnedBytes: number;
53
+ isComplete: boolean;
54
+ continuation: string | null;
55
+ }
56
+
57
+ // Zod schema for parsing bundle.json with snake_case keys
58
+ const metricsSchema = z.object({
59
+ wall_time_seconds: z.number().nullable(),
60
+ input_tokens: z.number().int().nullable(),
61
+ output_tokens: z.number().int().nullable(),
62
+ total_tokens: z.number().int().nullable(),
63
+ cost_usd: z.number().nullable(),
64
+ });
65
+
66
+ const gitProvenanceSchema = z.object({
67
+ repo_root: z.string(),
68
+ starting_branch: z.string(),
69
+ starting_commit: z.string(),
70
+ remotes: z.record(z.string(), z.string()),
71
+ });
72
+
73
+ const runBundleSchema = z.object({
74
+ schema_version: z.number().int(),
75
+ run_id: z.string(),
76
+ status: z.enum(["success", "failed"]),
77
+ started_at: z.string(),
78
+ finished_at: z.string(),
79
+ runner: z.string(),
80
+ runner_version: z.string().nullable(),
81
+ model: z.string().nullable(),
82
+ plan_source: z.string(),
83
+ workdir: z.string(),
84
+ git: gitProvenanceSchema,
85
+ metrics: metricsSchema,
86
+ result_branch: z.string().nullable(),
87
+ branch_created: z.boolean(),
88
+ runner_exit_code: z.number().int(),
89
+ error: z.string().nullable(),
90
+ });
91
+
92
+ export type RunBundleJson = z.input<typeof runBundleSchema>;
93
+
94
+ export function encodeMetrics(metrics: Metrics): z.input<typeof metricsSchema> {
95
+ return {
96
+ wall_time_seconds: metrics.wallTimeSeconds,
97
+ input_tokens: metrics.inputTokens,
98
+ output_tokens: metrics.outputTokens,
99
+ total_tokens: metrics.totalTokens,
100
+ cost_usd: metrics.costUsd,
101
+ };
102
+ }
103
+
104
+ export function encodeRunBundle(bundle: RunBundle): RunBundleJson {
105
+ return {
106
+ schema_version: bundle.schemaVersion,
107
+ run_id: bundle.runId,
108
+ status: bundle.status,
109
+ started_at: bundle.startedAt.toISOString(),
110
+ finished_at: bundle.finishedAt.toISOString(),
111
+ runner: bundle.runner,
112
+ runner_version: bundle.runnerVersion,
113
+ model: bundle.model,
114
+ plan_source: bundle.planSource,
115
+ workdir: bundle.workdir,
116
+ git: {
117
+ repo_root: bundle.git.repoRoot,
118
+ starting_branch: bundle.git.startingBranch,
119
+ starting_commit: bundle.git.startingCommit,
120
+ remotes: bundle.git.remotes,
121
+ },
122
+ metrics: encodeMetrics(bundle.metrics),
123
+ result_branch: bundle.resultBranch,
124
+ branch_created: bundle.branchCreated,
125
+ runner_exit_code: bundle.runnerExitCode,
126
+ error: bundle.error,
127
+ };
128
+ }
129
+
130
+ export function parseRunBundle(data: unknown): RunBundle {
131
+ const parsed = runBundleSchema.parse(data);
132
+ return {
133
+ schemaVersion: parsed.schema_version,
134
+ runId: parsed.run_id,
135
+ status: parsed.status,
136
+ startedAt: new Date(parsed.started_at),
137
+ finishedAt: new Date(parsed.finished_at),
138
+ runner: parsed.runner,
139
+ runnerVersion: parsed.runner_version,
140
+ model: parsed.model,
141
+ planSource: parsed.plan_source,
142
+ workdir: parsed.workdir,
143
+ git: {
144
+ repoRoot: parsed.git.repo_root,
145
+ startingBranch: parsed.git.starting_branch,
146
+ startingCommit: parsed.git.starting_commit,
147
+ remotes: parsed.git.remotes,
148
+ },
149
+ metrics: {
150
+ wallTimeSeconds: parsed.metrics.wall_time_seconds,
151
+ inputTokens: parsed.metrics.input_tokens,
152
+ outputTokens: parsed.metrics.output_tokens,
153
+ totalTokens: parsed.metrics.total_tokens,
154
+ costUsd: parsed.metrics.cost_usd,
155
+ },
156
+ resultBranch: parsed.result_branch,
157
+ branchCreated: parsed.branch_created,
158
+ runnerExitCode: parsed.runner_exit_code,
159
+ error: parsed.error,
160
+ };
161
+ }