@zhivex-ai/harness 0.6.1 → 0.8.0

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/dist/cli.js CHANGED
@@ -3,21 +3,369 @@
3
3
 
4
4
  // src/cli.ts
5
5
  import { createInterface } from "readline/promises";
6
+ import { randomUUID as randomUUID5 } from "crypto";
6
7
  import { constants as fsConstants } from "fs";
7
- import { access, lstat as lstat6, readFile as readFile4, stat as stat3 } from "fs/promises";
8
- import path8 from "path";
8
+ import { access, lstat as lstat7, readFile as readFile4, stat as stat4 } from "fs/promises";
9
+ import path9 from "path";
9
10
  import {
10
11
  getAgentBudgetStatus as getAgentBudgetStatus3
11
12
  } from "@zhivex-ai/agents";
12
13
 
13
14
  // src/config.ts
14
- import { createHash } from "crypto";
15
+ import { createHash as createHash2 } from "crypto";
15
16
  import { realpathSync } from "fs";
16
17
  import path from "path";
18
+
19
+ // src/providers.ts
20
+ import { createHash } from "crypto";
21
+ import { createGemini } from "@zhivex-ai/gemini";
17
22
  import { createMeta } from "@zhivex-ai/meta";
18
23
  import { createOpenAI } from "@zhivex-ai/openai";
19
24
  import { createQwen } from "@zhivex-ai/qwen";
20
- var PROVIDERS = ["meta", "qwen", "openai"];
25
+ var PROVIDERS = ["meta", "qwen", "openai", "gemini"];
26
+ var HARNESS_PROVIDER_CAPABILITIES = [
27
+ "streaming",
28
+ "tool-calling",
29
+ "approval-resume"
30
+ ];
31
+ var QWEN_REGIONS = [
32
+ "singapore",
33
+ "beijing",
34
+ "hong-kong",
35
+ "tokyo",
36
+ "frankfurt",
37
+ "virginia"
38
+ ];
39
+ var PROVIDER_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
40
+ var ENVIRONMENT_VARIABLE = /^[A-Z_][A-Z0-9_]{0,127}$/;
41
+ var DIAGNOSTIC_KEY = /^[a-z][A-Za-z0-9]{0,63}$/;
42
+ var requiredText = (name, value) => {
43
+ const normalized = value.trim();
44
+ if (!normalized || /[\u0000-\u001f\u007f]/.test(normalized)) {
45
+ throw new Error(`${name} must be a non-empty printable string.`);
46
+ }
47
+ return normalized;
48
+ };
49
+ var environmentVariable = (name, value) => {
50
+ if (!ENVIRONMENT_VARIABLE.test(value)) {
51
+ throw new Error(`${name} must be a valid environment variable name.`);
52
+ }
53
+ return value;
54
+ };
55
+ var diagnosticKey = (value) => {
56
+ if (!DIAGNOSTIC_KEY.test(value)) {
57
+ throw new Error(`Invalid provider diagnostic key: ${value}.`);
58
+ }
59
+ return value;
60
+ };
61
+ var normalizedDescriptor = (descriptor) => {
62
+ const id = descriptor.id.trim().toLowerCase();
63
+ if (descriptor.id !== id || !PROVIDER_ID.test(id)) {
64
+ throw new Error(`Invalid provider id: ${descriptor.id}. Use 1-64 lowercase letters, digits, dot, underscore, or hyphen.`);
65
+ }
66
+ if (!(descriptor.support === "certified" || descriptor.support === "provisional")) {
67
+ throw new Error(`Invalid support level for provider ${id}.`);
68
+ }
69
+ const credentialNames = [...new Set(descriptor.credentialNames.map((name) => environmentVariable(`Credential for provider ${id}`, name)))];
70
+ const capabilities = [...new Set(descriptor.capabilities)];
71
+ const invalidCapability = capabilities.find((capability) => !HARNESS_PROVIDER_CAPABILITIES.includes(capability));
72
+ if (invalidCapability) {
73
+ throw new Error(`Invalid capability for provider ${id}: ${invalidCapability}.`);
74
+ }
75
+ return Object.freeze({
76
+ id,
77
+ name: requiredText(`Name for provider ${id}`, descriptor.name),
78
+ defaultModel: requiredText(`Default model for provider ${id}`, descriptor.defaultModel),
79
+ credentialNames: Object.freeze(credentialNames),
80
+ capabilities: Object.freeze(capabilities),
81
+ support: descriptor.support
82
+ });
83
+ };
84
+ var normalizedDiagnostics = (provider, diagnostics) => {
85
+ const endpointEnvironmentVariable = diagnostics?.endpointEnvironmentVariable === undefined ? undefined : environmentVariable(`Endpoint diagnostic for provider ${provider}`, diagnostics.endpointEnvironmentVariable);
86
+ const seenKeys = new Set;
87
+ const presence = (diagnostics?.presence ?? []).map((entry) => {
88
+ const key = diagnosticKey(entry.key);
89
+ if (seenKeys.has(key)) {
90
+ throw new Error(`Duplicate diagnostic key for provider ${provider}: ${key}.`);
91
+ }
92
+ seenKeys.add(key);
93
+ return Object.freeze({
94
+ key,
95
+ environmentVariable: environmentVariable(`Presence diagnostic for provider ${provider}`, entry.environmentVariable)
96
+ });
97
+ });
98
+ const enums = (diagnostics?.enums ?? []).map((entry) => {
99
+ const key = diagnosticKey(entry.key);
100
+ if (seenKeys.has(key)) {
101
+ throw new Error(`Duplicate diagnostic key for provider ${provider}: ${key}.`);
102
+ }
103
+ seenKeys.add(key);
104
+ const allowedValues = [...new Set(entry.allowedValues.map((value) => requiredText(`Allowed diagnostic value for provider ${provider}`, value)))];
105
+ if (allowedValues.length === 0) {
106
+ throw new Error(`Enum diagnostic ${key} for provider ${provider} needs allowed values.`);
107
+ }
108
+ return Object.freeze({
109
+ key,
110
+ environmentVariable: environmentVariable(`Enum diagnostic for provider ${provider}`, entry.environmentVariable),
111
+ allowedValues: Object.freeze(allowedValues)
112
+ });
113
+ });
114
+ return Object.freeze({
115
+ ...endpointEnvironmentVariable ? { endpointEnvironmentVariable } : {},
116
+ ...presence.length > 0 ? { presence: Object.freeze(presence) } : {},
117
+ ...enums.length > 0 ? { enums: Object.freeze(enums) } : {}
118
+ });
119
+ };
120
+ var endpointConfiguration = (value) => {
121
+ const configured = Boolean(value?.trim());
122
+ if (!configured) {
123
+ return {
124
+ customEndpoint: false,
125
+ endpointValid: true,
126
+ endpointSecure: true
127
+ };
128
+ }
129
+ try {
130
+ const endpoint = new URL(value.trim());
131
+ const supportedProtocol = endpoint.protocol === "https:" || endpoint.protocol === "http:";
132
+ return {
133
+ customEndpoint: true,
134
+ endpointValid: supportedProtocol && !endpoint.username && !endpoint.password && !endpoint.hash && !endpoint.search,
135
+ endpointSecure: endpoint.protocol === "https:"
136
+ };
137
+ } catch {
138
+ return {
139
+ customEndpoint: true,
140
+ endpointValid: false,
141
+ endpointSecure: false
142
+ };
143
+ }
144
+ };
145
+ var createCredentials = (env, provider, names) => Object.freeze({
146
+ names,
147
+ get: (name) => {
148
+ if (!names.includes(name)) {
149
+ throw new Error(`Credential ${name} is not declared for provider ${provider}.`);
150
+ }
151
+ return env[name]?.trim() || undefined;
152
+ },
153
+ first: () => {
154
+ for (const name of names) {
155
+ const value = env[name]?.trim();
156
+ if (value) {
157
+ return value;
158
+ }
159
+ }
160
+ return;
161
+ },
162
+ require: () => {
163
+ for (const name of names) {
164
+ const value = env[name]?.trim();
165
+ if (value) {
166
+ return value;
167
+ }
168
+ }
169
+ throw new Error(names.length === 0 ? `Provider ${provider} does not declare a credential.` : `Missing credentials for ${provider}. Set ${names.join(" or ")} in the environment.`);
170
+ }
171
+ });
172
+ var normalizeRegistration = (registration) => {
173
+ if (typeof registration.factory !== "function") {
174
+ throw new Error("Provider registration factory must be a function.");
175
+ }
176
+ const descriptor = normalizedDescriptor(registration.descriptor);
177
+ return Object.freeze({
178
+ descriptor,
179
+ factory: registration.factory,
180
+ diagnostics: normalizedDiagnostics(descriptor.id, registration.diagnostics)
181
+ });
182
+ };
183
+ var providerConfiguration = (diagnostics, env) => {
184
+ const configuration = endpointConfiguration(diagnostics.endpointEnvironmentVariable ? env[diagnostics.endpointEnvironmentVariable] : undefined);
185
+ for (const entry of diagnostics.presence ?? []) {
186
+ configuration[`${entry.key}Configured`] = Boolean(env[entry.environmentVariable]?.trim());
187
+ }
188
+ for (const entry of diagnostics.enums ?? []) {
189
+ const value = env[entry.environmentVariable]?.trim();
190
+ configuration[`${entry.key}Configured`] = Boolean(value);
191
+ configuration[`${entry.key}Valid`] = !value || entry.allowedValues.includes(value);
192
+ }
193
+ return Object.freeze(configuration);
194
+ };
195
+ var parseWithRegistrations = (value, registrations) => {
196
+ const defaultProvider = registrations.has("openai") ? "openai" : registrations.keys().next().value;
197
+ const normalized = (value ?? defaultProvider ?? "").trim().toLowerCase();
198
+ if (registrations.has(normalized)) {
199
+ return normalized;
200
+ }
201
+ throw new Error(`Unknown provider: ${value}. Use ${[...registrations.keys()].join(", ")}.`);
202
+ };
203
+ var createProviderRegistry = (registrations) => {
204
+ if (registrations.length === 0) {
205
+ throw new Error("Provider registry must contain at least one provider.");
206
+ }
207
+ const entries = new Map;
208
+ for (const registration of registrations) {
209
+ const normalized = normalizeRegistration(registration);
210
+ if (entries.has(normalized.descriptor.id)) {
211
+ throw new Error(`Duplicate provider registration: ${normalized.descriptor.id}.`);
212
+ }
213
+ entries.set(normalized.descriptor.id, normalized);
214
+ }
215
+ const frozenRegistrations = Object.freeze([...entries.values()]);
216
+ const descriptors = Object.freeze(frozenRegistrations.map((entry) => entry.descriptor));
217
+ const registry = Object.freeze({
218
+ descriptors,
219
+ has: (provider) => entries.has(provider.trim().toLowerCase()),
220
+ parse: (value) => parseWithRegistrations(value, entries),
221
+ descriptor: (provider) => {
222
+ const registration = entries.get(provider);
223
+ if (!registration) {
224
+ throw new Error(`No descriptor is available for provider ${provider}.`);
225
+ }
226
+ return registration.descriptor;
227
+ },
228
+ createModel: (config, env = process.env) => {
229
+ const registration = entries.get(config.provider);
230
+ if (!registration) {
231
+ throw new Error(`No factory is available for provider ${config.provider}.`);
232
+ }
233
+ return registration.factory(Object.freeze({
234
+ provider: config.provider,
235
+ model: config.model,
236
+ env,
237
+ credentials: createCredentials(env, config.provider, registration.descriptor.credentialNames)
238
+ }));
239
+ },
240
+ availability: (env = process.env) => Object.freeze(frozenRegistrations.map((registration) => {
241
+ const credentials = Object.freeze(registration.descriptor.credentialNames.map((name) => Object.freeze({ name, present: Boolean(env[name]?.trim()) })));
242
+ return Object.freeze({
243
+ ...registration.descriptor,
244
+ configured: credentials.length === 0 || credentials.some((credential) => credential.present),
245
+ credentials,
246
+ configuration: providerConfiguration(registration.diagnostics, env)
247
+ });
248
+ })),
249
+ transportFingerprint: (env = process.env) => {
250
+ const transport = frozenRegistrations.map((registration) => {
251
+ const names = [
252
+ registration.diagnostics.endpointEnvironmentVariable,
253
+ ...(registration.diagnostics.presence ?? []).map((entry) => entry.environmentVariable),
254
+ ...(registration.diagnostics.enums ?? []).map((entry) => entry.environmentVariable)
255
+ ].filter((name) => Boolean(name));
256
+ return {
257
+ provider: registration.descriptor.id,
258
+ values: names.map((name) => [name, env[name]?.trim() ?? ""])
259
+ };
260
+ });
261
+ return `sha256:${createHash("sha256").update(JSON.stringify(transport)).digest("hex")}`;
262
+ },
263
+ extend: (additional) => createProviderRegistry([
264
+ ...frozenRegistrations,
265
+ ...additional
266
+ ])
267
+ });
268
+ return registry;
269
+ };
270
+ var optionalQwenRegion = (value) => {
271
+ if (!value) {
272
+ return;
273
+ }
274
+ if (QWEN_REGIONS.includes(value)) {
275
+ return value;
276
+ }
277
+ throw new Error(`Invalid QWEN_REGION: ${value}.`);
278
+ };
279
+ var BUILTIN_PROVIDER_REGISTRATIONS = Object.freeze([
280
+ {
281
+ descriptor: {
282
+ id: "meta",
283
+ name: "Meta Model API",
284
+ defaultModel: "muse-spark-1.2",
285
+ credentialNames: ["MODEL_API_KEY"],
286
+ capabilities: HARNESS_PROVIDER_CAPABILITIES,
287
+ support: "certified"
288
+ },
289
+ diagnostics: { endpointEnvironmentVariable: "META_BASE_URL" },
290
+ factory: ({ model, env, credentials }) => {
291
+ const baseURL = env.META_BASE_URL?.trim();
292
+ return createMeta({
293
+ apiKey: credentials.require(),
294
+ ...baseURL ? { baseURL } : {}
295
+ })(model);
296
+ }
297
+ },
298
+ {
299
+ descriptor: {
300
+ id: "qwen",
301
+ name: "Qwen / Alibaba Cloud Model Studio",
302
+ defaultModel: "qwen3.8-max",
303
+ credentialNames: ["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
304
+ capabilities: HARNESS_PROVIDER_CAPABILITIES,
305
+ support: "certified"
306
+ },
307
+ diagnostics: {
308
+ endpointEnvironmentVariable: "QWEN_BASE_URL",
309
+ presence: [{ key: "workspaceId", environmentVariable: "QWEN_WORKSPACE_ID" }],
310
+ enums: [{
311
+ key: "region",
312
+ environmentVariable: "QWEN_REGION",
313
+ allowedValues: QWEN_REGIONS
314
+ }]
315
+ },
316
+ factory: ({ model, env, credentials }) => {
317
+ const baseURL = env.QWEN_BASE_URL?.trim();
318
+ const workspaceId = env.QWEN_WORKSPACE_ID?.trim();
319
+ const region = optionalQwenRegion(env.QWEN_REGION?.trim());
320
+ return createQwen({
321
+ apiKey: credentials.require(),
322
+ ...baseURL ? { baseURL } : {},
323
+ ...workspaceId ? { workspaceId } : {},
324
+ ...region ? { region } : {}
325
+ })(model);
326
+ }
327
+ },
328
+ {
329
+ descriptor: {
330
+ id: "openai",
331
+ name: "OpenAI",
332
+ defaultModel: "gpt-5.6-luna",
333
+ credentialNames: ["OPENAI_API_KEY"],
334
+ capabilities: HARNESS_PROVIDER_CAPABILITIES,
335
+ support: "certified"
336
+ },
337
+ diagnostics: { endpointEnvironmentVariable: "OPENAI_BASE_URL" },
338
+ factory: ({ model, env, credentials }) => {
339
+ const baseURL = env.OPENAI_BASE_URL?.trim();
340
+ return createOpenAI({
341
+ apiKey: credentials.require(),
342
+ ...baseURL ? { baseURL } : {}
343
+ })(model);
344
+ }
345
+ },
346
+ {
347
+ descriptor: {
348
+ id: "gemini",
349
+ name: "Google Gemini",
350
+ defaultModel: "gemini-3.6-flash",
351
+ credentialNames: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
352
+ capabilities: HARNESS_PROVIDER_CAPABILITIES,
353
+ support: "provisional"
354
+ },
355
+ diagnostics: { endpointEnvironmentVariable: "GEMINI_BASE_URL" },
356
+ factory: ({ model, env, credentials }) => {
357
+ const baseURL = env.GEMINI_BASE_URL?.trim();
358
+ return createGemini({
359
+ apiKey: credentials.require(),
360
+ ...baseURL ? { baseURL } : {}
361
+ })(model);
362
+ }
363
+ }
364
+ ]);
365
+ var DEFAULT_PROVIDER_REGISTRY = createProviderRegistry(BUILTIN_PROVIDER_REGISTRATIONS);
366
+ var PROVIDER_DESCRIPTORS = DEFAULT_PROVIDER_REGISTRY.descriptors;
367
+
368
+ // src/config.ts
21
369
  var DEFAULT_ALLOWED_CHECKS = ["test", "typecheck", "lint", "build"];
22
370
  var HARNESS_CONFIG_SCHEMA_VERSION = 4;
23
371
  var HARNESS_STORE_BACKENDS = ["sqlite", "file"];
@@ -73,45 +421,6 @@ var DEFAULT_SUBAGENT_BUDGET = {
73
421
  maxTotalTokens: 36000,
74
422
  includeChildRuns: false
75
423
  };
76
- var HARNESS_PROVIDER_CAPABILITIES = [
77
- "streaming",
78
- "tool-calling",
79
- "approval-resume"
80
- ];
81
- var PROVIDER_DESCRIPTORS = [
82
- {
83
- id: "meta",
84
- name: "Meta Model API",
85
- defaultModel: "muse-spark-1.2",
86
- credentialNames: ["MODEL_API_KEY"],
87
- capabilities: HARNESS_PROVIDER_CAPABILITIES,
88
- support: "certified"
89
- },
90
- {
91
- id: "qwen",
92
- name: "Qwen / Alibaba Cloud Model Studio",
93
- defaultModel: "qwen3.8-max",
94
- credentialNames: ["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
95
- capabilities: HARNESS_PROVIDER_CAPABILITIES,
96
- support: "certified"
97
- },
98
- {
99
- id: "openai",
100
- name: "OpenAI",
101
- defaultModel: "gpt-5.4",
102
- credentialNames: ["OPENAI_API_KEY"],
103
- capabilities: HARNESS_PROVIDER_CAPABILITIES,
104
- support: "certified"
105
- }
106
- ];
107
- var QWEN_REGIONS = [
108
- "singapore",
109
- "beijing",
110
- "hong-kong",
111
- "tokyo",
112
- "frankfurt",
113
- "virginia"
114
- ];
115
424
  var integerOption = (name, input, envValue, fallback, minimum, maximum) => {
116
425
  const value = input ?? (envValue === undefined ? fallback : Number(envValue));
117
426
  if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
@@ -140,7 +449,7 @@ var nonNegativeNumberOption = (name, input, envValue) => {
140
449
  }
141
450
  return value;
142
451
  };
143
- var defaultHarnessNamespace = (workspace) => `workspace-${createHash("sha256").update(workspace).digest("hex").slice(0, 16)}`;
452
+ var defaultHarnessNamespace = (workspace) => `workspace-${createHash2("sha256").update(workspace).digest("hex").slice(0, 16)}`;
144
453
  var canonicalStateDirectory = (requestedWorkspace, canonicalWorkspace, configured) => {
145
454
  if (!configured) {
146
455
  return path.join(canonicalWorkspace, ".zhivex-harness", "runs");
@@ -242,26 +551,14 @@ var canonicalWorkspaceFile = (requestedWorkspace, canonicalWorkspace, configured
242
551
  const insideRequestedWorkspace = relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
243
552
  return insideRequestedWorkspace ? path.resolve(canonicalWorkspace, relative) : requestedPath;
244
553
  };
245
- var parseProvider = (value) => {
246
- const normalized = (value ?? "openai").trim().toLowerCase();
247
- if (PROVIDERS.includes(normalized)) {
248
- return normalized;
249
- }
250
- throw new Error(`Unknown provider: ${value}. Use meta, qwen, or openai.`);
251
- };
252
- var providerDescriptor = (provider) => {
253
- const descriptor = PROVIDER_DESCRIPTORS.find((entry) => entry.id === provider);
254
- if (!descriptor) {
255
- throw new Error(`No descriptor is available for provider ${provider}.`);
256
- }
257
- return descriptor;
258
- };
259
- var resolveHarnessConfig = (input = {}) => {
554
+ var parseProvider = (value, registry = DEFAULT_PROVIDER_REGISTRY) => registry.parse(value);
555
+ var providerDescriptor = (provider, registry = DEFAULT_PROVIDER_REGISTRY) => registry.descriptor(provider);
556
+ var resolveHarnessConfig = (input = {}, registry = DEFAULT_PROVIDER_REGISTRY) => {
260
557
  if (input.schemaVersion !== undefined && input.schemaVersion !== HARNESS_CONFIG_SCHEMA_VERSION) {
261
558
  throw new Error(`Unsupported config schema version: ${input.schemaVersion}. Expected ${HARNESS_CONFIG_SCHEMA_VERSION}.`);
262
559
  }
263
- const provider = parseProvider(input.provider ?? process.env.ZHIVEX_HARNESS_PROVIDER);
264
- const descriptor = providerDescriptor(provider);
560
+ const provider = parseProvider(input.provider ?? process.env.ZHIVEX_HARNESS_PROVIDER, registry);
561
+ const descriptor = providerDescriptor(provider, registry);
265
562
  const requestedWorkspace = path.resolve(input.workspace ?? process.cwd());
266
563
  let workspace = requestedWorkspace;
267
564
  try {
@@ -355,105 +652,11 @@ var resolveHarnessConfig = (input = {}) => {
355
652
  ...mcpConfigPath ? { mcpConfigPath } : {}
356
653
  };
357
654
  };
358
- var requiredCredential = (env, provider, names) => {
359
- for (const name of names) {
360
- const value = env[name]?.trim();
361
- if (value) {
362
- return value;
363
- }
364
- }
365
- throw new Error(`Missing credentials for ${provider}. Set ${names.join(" or ")} in the environment.`);
366
- };
367
- var optionalQwenRegion = (value) => {
368
- if (!value) {
369
- return;
370
- }
371
- if (QWEN_REGIONS.includes(value)) {
372
- return value;
373
- }
374
- throw new Error(`Invalid QWEN_REGION: ${value}.`);
375
- };
376
- var createProviderModel = (config, env = process.env) => {
377
- switch (config.provider) {
378
- case "meta": {
379
- const baseURL = env.META_BASE_URL?.trim();
380
- const provider = createMeta({
381
- apiKey: requiredCredential(env, "meta", ["MODEL_API_KEY"]),
382
- ...baseURL ? { baseURL } : {}
383
- });
384
- return provider(config.model);
385
- }
386
- case "qwen": {
387
- const baseURL = env.QWEN_BASE_URL?.trim();
388
- const workspaceId = env.QWEN_WORKSPACE_ID?.trim();
389
- const region = optionalQwenRegion(env.QWEN_REGION?.trim());
390
- const provider = createQwen({
391
- apiKey: requiredCredential(env, "qwen", ["DASHSCOPE_API_KEY", "QWEN_API_KEY"]),
392
- ...baseURL ? { baseURL } : {},
393
- ...workspaceId ? { workspaceId } : {},
394
- ...region ? { region } : {}
395
- });
396
- return provider(config.model);
397
- }
398
- case "openai": {
399
- const baseURL = env.OPENAI_BASE_URL?.trim();
400
- const provider = createOpenAI({
401
- apiKey: requiredCredential(env, "openai", ["OPENAI_API_KEY"]),
402
- ...baseURL ? { baseURL } : {}
403
- });
404
- return provider(config.model);
405
- }
406
- }
407
- };
408
- var endpointConfiguration = (value) => {
409
- const configured = Boolean(value?.trim());
410
- if (!configured) {
411
- return {
412
- customEndpoint: false,
413
- endpointValid: true,
414
- endpointSecure: true
415
- };
416
- }
417
- try {
418
- const endpoint = new URL(value.trim());
419
- const supportedProtocol = endpoint.protocol === "https:" || endpoint.protocol === "http:";
420
- return {
421
- customEndpoint: true,
422
- endpointValid: supportedProtocol && !endpoint.username && !endpoint.password && !endpoint.hash,
423
- endpointSecure: endpoint.protocol === "https:"
424
- };
425
- } catch {
426
- return {
427
- customEndpoint: true,
428
- endpointValid: false,
429
- endpointSecure: false
430
- };
431
- }
432
- };
433
- var providerAvailability = (env = process.env) => PROVIDER_DESCRIPTORS.map((descriptor) => {
434
- const credentials = descriptor.credentialNames.map((name) => ({
435
- name,
436
- present: Boolean(env[name]?.trim())
437
- }));
438
- const endpointVariable = descriptor.id === "meta" ? "META_BASE_URL" : descriptor.id === "qwen" ? "QWEN_BASE_URL" : "OPENAI_BASE_URL";
439
- const regionValue = descriptor.id === "qwen" ? env.QWEN_REGION?.trim() : undefined;
440
- return {
441
- ...descriptor,
442
- configured: credentials.some((credential) => credential.present),
443
- credentials,
444
- configuration: {
445
- ...endpointConfiguration(env[endpointVariable]),
446
- ...descriptor.id === "qwen" ? {
447
- workspaceIdConfigured: Boolean(env.QWEN_WORKSPACE_ID?.trim()),
448
- regionConfigured: Boolean(regionValue),
449
- regionValid: !regionValue || QWEN_REGIONS.includes(regionValue)
450
- } : {}
451
- }
452
- };
453
- });
655
+ var createProviderModel = (config, env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => registry.createModel(config, env);
656
+ var providerAvailability = (env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => registry.availability(env);
454
657
 
455
658
  // src/harness.ts
456
- import { createHash as createHash6 } from "crypto";
659
+ import { createHash as createHash7 } from "crypto";
457
660
  import {
458
661
  Agent,
459
662
  applySafetyPolicyToAgent as applySafetyPolicyToAgent2,
@@ -523,7 +726,7 @@ var selectHarnessModel = (candidates, required) => {
523
726
  };
524
727
 
525
728
  // src/edit-contracts.ts
526
- import { createHash as createHash2 } from "crypto";
729
+ import { createHash as createHash3 } from "crypto";
527
730
  import path2 from "path";
528
731
  import { z } from "zod";
529
732
  var EDIT_CONTRACT_SCHEMA_VERSION = 1;
@@ -623,7 +826,7 @@ var mutationAuditEntrySchema = z.strictObject({
623
826
  timestamp: z.iso.datetime(),
624
827
  quarantineId: z.string().min(1).max(200).optional()
625
828
  });
626
- var digest = (value) => `sha256:${createHash2("sha256").update(value).digest("hex")}`;
829
+ var digest = (value) => `sha256:${createHash3("sha256").update(value).digest("hex")}`;
627
830
  var canonicalProposalPayload = (changes) => ({
628
831
  schemaVersion: EDIT_CONTRACT_SCHEMA_VERSION,
629
832
  kind: "edit-proposal",
@@ -658,7 +861,7 @@ var editContractDocument = (kind, result) => ({
658
861
  });
659
862
 
660
863
  // src/workspace.ts
661
- import { createHash as createHash3, randomUUID } from "crypto";
864
+ import { createHash as createHash4, randomUUID } from "crypto";
662
865
  import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "fs/promises";
663
866
  import path3 from "path";
664
867
  var HARD_IGNORES = new Set([".git", ".next", ".turbo", ".zhivex-harness", "coverage", "dist", "node_modules"]);
@@ -676,7 +879,7 @@ var isInside = (root, candidate) => {
676
879
  return relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative);
677
880
  };
678
881
  var wirePath = (value) => value.split(path3.sep).join("/");
679
- var digestBytes = (value) => `sha256:${createHash3("sha256").update(value).digest("hex")}`;
882
+ var digestBytes = (value) => `sha256:${createHash4("sha256").update(value).digest("hex")}`;
680
883
  var truncate = (value, maxLength = MAX_TOOL_OUTPUT) => value.length <= maxLength ? value : `${value.slice(0, maxLength)}
681
884
  \u2026 output truncated (${value.length - maxLength} characters omitted)`;
682
885
  var cursorEncode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
@@ -2173,7 +2376,7 @@ var createHarnessMcpTools = async (configuration, options = {}) => {
2173
2376
  var mcpConfigurationFingerprintInput = (configuration) => serializeJsonValue(configuration);
2174
2377
 
2175
2378
  // src/orchestration.ts
2176
- import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
2379
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
2177
2380
  import {
2178
2381
  applySafetyPolicyToAgent,
2179
2382
  createBudgetGuard,
@@ -2220,7 +2423,7 @@ var childHarnessBinding = (parent, profile, model, toolNames, config) => ({
2220
2423
  schemaVersion: 1,
2221
2424
  id: `${parent.id}-${profile}`,
2222
2425
  version: parent.version,
2223
- fingerprint: `sha256:${createHash4("sha256").update(JSON.stringify({
2426
+ fingerprint: `sha256:${createHash5("sha256").update(JSON.stringify({
2224
2427
  parent: parent.fingerprint,
2225
2428
  profile,
2226
2429
  model: {
@@ -2344,7 +2547,7 @@ var runHarnessReviewGroup = async (runtime, input, profiles = ["explorer", "revi
2344
2547
  };
2345
2548
 
2346
2549
  // src/execution-environment.ts
2347
- import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
2550
+ import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
2348
2551
  import {
2349
2552
  chmod as chmod2,
2350
2553
  lstat as lstat5,
@@ -2384,8 +2587,8 @@ var BUILT_IN_TOOL_NAMES = new Set([
2384
2587
  "apply_environment_patch",
2385
2588
  "environment_status"
2386
2589
  ]);
2387
- var digest2 = (value) => `sha256:${createHash5("sha256").update(value).digest("hex")}`;
2388
- var runHash = (runId) => createHash5("sha256").update(runId).digest("hex").slice(0, 24);
2590
+ var digest2 = (value) => `sha256:${createHash6("sha256").update(value).digest("hex")}`;
2591
+ var runHash = (runId) => createHash6("sha256").update(runId).digest("hex").slice(0, 24);
2389
2592
  var isInside3 = (root, candidate) => {
2390
2593
  const relative = path7.relative(root, candidate);
2391
2594
  return relative === "" || relative !== ".." && !relative.startsWith(`..${path7.sep}`) && !path7.isAbsolute(relative);
@@ -3357,11 +3560,11 @@ var describeOciCommand = (command, args) => commandDisplay([command, ...args]);
3357
3560
  // src/harness.ts
3358
3561
  var APPROVAL_VERSION = "2026-08-17-v5";
3359
3562
  var TOOL_CONTRACT_VERSION = "enforced-execution-v1";
3360
- var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, executionEnvironment) => ({
3563
+ var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, providerTransportFingerprint, executionEnvironment) => ({
3361
3564
  schemaVersion: 1,
3362
3565
  id: "zhivex-harness",
3363
3566
  version: HARNESS_VERSION,
3364
- fingerprint: `sha256:${createHash6("sha256").update(JSON.stringify({
3567
+ fingerprint: `sha256:${createHash7("sha256").update(JSON.stringify({
3365
3568
  configSchemaVersion: HARNESS_CONFIG_SCHEMA_VERSION,
3366
3569
  approvalVersion: APPROVAL_VERSION,
3367
3570
  toolContractVersion: TOOL_CONTRACT_VERSION,
@@ -3369,6 +3572,7 @@ var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, exe
3369
3572
  provider: config.provider,
3370
3573
  model: config.model,
3371
3574
  runtimeModel: inspectHarnessModelCapabilities(model),
3575
+ providerTransportFingerprint,
3372
3576
  subagentModels: Object.fromEntries(config.orchestration.profiles.map((profile) => [
3373
3577
  profile,
3374
3578
  inspectHarnessModelCapabilities(subagentModels?.[profile] ?? model)
@@ -3390,6 +3594,7 @@ Rules:
3390
3594
  - Make the smallest coherent change that fully addresses the task.
3391
3595
  - For every file edit, first read its digest and call propose_edits. Apply exactly that reviewed proposal with apply_patch.
3392
3596
  - apply_patch, move_file, quarantine_file, restore_file, and run_check require explicit approval from the operator.
3597
+ - Calling an approval-gated tool is how you request that approval: call the tool with its complete reviewed arguments, then let the runtime pause before execution. Never replace the tool call with a textual approval request.
3393
3598
  - With enforced OCI execution enabled, workspace tools operate on an ephemeral snapshot. Use inspect_environment_patch and obtain a separate approval through apply_environment_patch before changing the host workspace.
3394
3599
  - run_environment_command executes one allowlisted argv command without a host shell. Network, privileges, resources, environment variables, and output are bounded by the OCI policy.
3395
3600
  - Never overwrite stale content. If an expected digest no longer matches, inspect the file again and create a new proposal.
@@ -3485,7 +3690,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
3485
3690
  }),
3486
3691
  apply_patch: tool({
3487
3692
  name: "apply_patch",
3488
- description: "Atomically apply one reviewed multi-file proposal. Every existing file requires its exact expected digest; expectedDigest=null is create-only.",
3693
+ description: "Request approval to atomically apply one reviewed multi-file proposal; the runtime pauses before execution. Every existing file requires its exact expected digest; expectedDigest=null is create-only. Call this tool instead of asking for approval in text.",
3489
3694
  schema: applyEditProposalInputSchema,
3490
3695
  ...mutationApproval,
3491
3696
  execute: async (input, context) => {
@@ -3591,32 +3796,49 @@ var createExecutionEnvironmentTools = (workspace) => ({
3591
3796
  })
3592
3797
  });
3593
3798
  var estimateMessageTokens = (messages) => Math.max(1, Math.ceil(JSON.stringify(messages).length / 4));
3594
- var createHarnessCompactor = () => {
3799
+ var summarizeHarnessMessages = (messages) => {
3595
3800
  const redaction = createRedactionPolicy2({ includeEmails: true });
3596
- return async ({ messages }) => {
3597
- const summaries = messages.map((message, index) => {
3598
- const record = message;
3599
- const parts = (record.parts ?? []).map((part) => {
3600
- if (part.type === "text" && typeof part.text === "string") {
3601
- const text = redaction.redactText(part.text).replace(/\s+/g, " ").trim();
3602
- return text.length > 160 ? `${text.slice(0, 160)}\u2026` : text;
3603
- }
3604
- if (part.type === "tool-call") {
3605
- const call = part.toolCall;
3606
- return `tool-call:${typeof call?.name === "string" ? call.name : "unknown"}`;
3607
- }
3608
- if (part.type === "tool-result") {
3609
- const result = part.toolResult;
3610
- return `tool-result:${typeof result?.toolName === "string" ? result.toolName : "unknown"}`;
3611
- }
3612
- return typeof part.type === "string" ? part.type : "part";
3613
- });
3614
- return `${index + 1}. ${record.role ?? "message"}: ${parts.join(" | ")}`;
3801
+ const summaries = messages.map((message, index) => {
3802
+ const record = message;
3803
+ const parts = (record.parts ?? []).map((part) => {
3804
+ if (part.type === "text" && typeof part.text === "string") {
3805
+ const text = redaction.redactText(part.text).replace(/\bBearer\s+\S+/gi, "Bearer [REDACTED]").replace(/\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)|api[_-]?key|access[_-]?token|password)\s*([=:])\s*(?:"[^"]*"|'[^']*'|\S+)/gi, "$1$2[REDACTED]").replace(/\b(?:sk|ghp|gho|github_pat)-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]").replace(/\s+/g, " ").trim();
3806
+ return text.length > 160 ? `${text.slice(0, 160)}\u2026` : text;
3807
+ }
3808
+ if (part.type === "tool-call") {
3809
+ const call = part.toolCall;
3810
+ return `tool-call:${typeof call?.name === "string" ? call.name : "unknown"}`;
3811
+ }
3812
+ if (part.type === "tool-result") {
3813
+ const result = part.toolResult;
3814
+ return `tool-result:${typeof result?.toolName === "string" ? result.toolName : "unknown"}`;
3815
+ }
3816
+ return typeof part.type === "string" ? part.type : "part";
3615
3817
  });
3616
- const summary = summaries.join(`
3818
+ return `${index + 1}. ${record.role ?? "message"}: ${parts.join(" | ")}`;
3819
+ });
3820
+ const summary = summaries.join(`
3617
3821
  `);
3822
+ return summary.length > 4000 ? `${summary.slice(0, 4000)}\u2026` : summary;
3823
+ };
3824
+ var compactHarnessMessages = (messages) => {
3825
+ if (messages.length === 0)
3826
+ return [];
3827
+ return [
3828
+ {
3829
+ role: "user",
3830
+ parts: [{
3831
+ type: "text",
3832
+ text: `[Compacted conversation context]
3833
+ ${summarizeHarnessMessages(messages)}`
3834
+ }]
3835
+ }
3836
+ ];
3837
+ };
3838
+ var createHarnessCompactor = () => {
3839
+ return async ({ messages }) => {
3618
3840
  return {
3619
- summary: summary.length > 4000 ? `${summary.slice(0, 4000)}\u2026` : summary,
3841
+ summary: summarizeHarnessMessages(messages),
3620
3842
  metadata: {
3621
3843
  strategy: "deterministic-redacted-transcript",
3622
3844
  sourceMessages: messages.length
@@ -3679,7 +3901,7 @@ var createProviderCompatibleBudget = (config) => {
3679
3901
  };
3680
3902
  };
3681
3903
  var createHarness = async (options = {}) => {
3682
- const config = resolveHarnessConfig(options);
3904
+ const config = resolveHarnessConfig(options, options.providerRegistry);
3683
3905
  const workspace = await Workspace.open(config.workspace);
3684
3906
  await validateStateDirectory(config.workspace, config.stateDirectory);
3685
3907
  const executionEnvironment = config.execution.backend === "oci" ? await createHarnessOciExecutionEnvironment({
@@ -3688,7 +3910,7 @@ var createHarness = async (options = {}) => {
3688
3910
  stateDirectory: config.stateDirectory,
3689
3911
  ...options.ociRuntimeAdapter ? { runtime: options.ociRuntimeAdapter } : {}
3690
3912
  }) : undefined;
3691
- const model = options.modelInstance ?? createProviderModel(config, options.env ?? process.env);
3913
+ const model = options.modelInstance ?? createProviderModel(config, options.env ?? process.env, options.providerRegistry);
3692
3914
  const capabilityRequirements = [...new Set([
3693
3915
  ...config.requiredCapabilities,
3694
3916
  ...config.orchestration.profiles.length > 0 || config.mcpConfigPath || options.mcpConfiguration ? ["tools", "streaming"] : []
@@ -3730,7 +3952,7 @@ var createHarness = async (options = {}) => {
3730
3952
  await options.onTelemetryEvent?.(event);
3731
3953
  };
3732
3954
  const costGuardrails = createCostGuardrails(config);
3733
- const binding = createHarnessBinding(config, mcpConfiguration, model, options.subagentModels, executionEnvironment);
3955
+ const binding = createHarnessBinding(config, mcpConfiguration, model, options.subagentModels, (options.providerRegistry ?? DEFAULT_PROVIDER_REGISTRY).transportFingerprint(options.env ?? process.env), executionEnvironment);
3734
3956
  const subagentRuntime = createHarnessSubagents({
3735
3957
  config,
3736
3958
  parentBinding: binding,
@@ -3862,33 +4084,851 @@ var appendUserMessage = (messages, text) => [
3862
4084
  import {
3863
4085
  estimateAgentRunCost
3864
4086
  } from "@zhivex-ai/agents/ops";
3865
- var CLI_JSON_SCHEMA_VERSION = 1;
3866
- var HARNESS_RESUME_METADATA_KEY = "zhivexHarnessResume";
3867
- var HARNESS_RESUME_METADATA_SCHEMA_VERSION = 1;
3868
- var CLI_EXIT_CODES = {
3869
- success: 0,
3870
- runtimeError: 1,
3871
- usageError: 2,
3872
- doctorFailed: 3
3873
- };
3874
- var COMMANDS = new Set(["run", "review", "chat", "providers", "doctor", "resume", "runs", "help", "version"]);
3875
- var RUNS_COMMANDS = new Set(["list", "inspect", "cancel", "cleanup", "export"]);
3876
- var RUN_STATUSES = new Set([
3877
- "queued",
3878
- "running",
3879
- "completed",
3880
- "suspended",
3881
- "waiting_approval",
3882
- "cancel_requested",
3883
- "failed",
3884
- "cancelled",
3885
- "timed_out"
3886
- ]);
3887
4087
 
3888
- class CliUsageError extends Error {
3889
- constructor(message) {
3890
- super(message);
3891
- this.name = "CliUsageError";
4088
+ // src/routing.ts
4089
+ var routeError = (value) => new Error(`Invalid route: ${value}. Use <profile>=<provider>[:<model>].`);
4090
+ var parseHarnessModelRoute = (value, registry = DEFAULT_PROVIDER_REGISTRY) => {
4091
+ const separator = value.indexOf("=");
4092
+ if (separator <= 0 || separator === value.length - 1) {
4093
+ throw routeError(value);
4094
+ }
4095
+ const profile = value.slice(0, separator).trim().toLowerCase();
4096
+ if (!HARNESS_SUBAGENT_PROFILES.includes(profile)) {
4097
+ throw routeError(value);
4098
+ }
4099
+ const target = value.slice(separator + 1).trim();
4100
+ const modelSeparator = target.indexOf(":");
4101
+ const providerInput = (modelSeparator === -1 ? target : target.slice(0, modelSeparator)).trim();
4102
+ const provider = registry.parse(providerInput);
4103
+ const configuredModel = modelSeparator === -1 ? "" : target.slice(modelSeparator + 1).trim();
4104
+ if (modelSeparator !== -1 && !configuredModel) {
4105
+ throw routeError(value);
4106
+ }
4107
+ const model = configuredModel || registry.descriptor(provider).defaultModel;
4108
+ if (/[\u0000-\u001f\u007f]/.test(model) || model.length > 256) {
4109
+ throw routeError(value);
4110
+ }
4111
+ return Object.freeze({
4112
+ profile,
4113
+ provider,
4114
+ model
4115
+ });
4116
+ };
4117
+ var resolveHarnessModelRoutes = (values = [], registry = DEFAULT_PROVIDER_REGISTRY) => {
4118
+ const routes = new Map;
4119
+ for (const value of values) {
4120
+ const route = parseHarnessModelRoute(value, registry);
4121
+ if (routes.has(route.profile)) {
4122
+ throw new Error(`Duplicate model route for ${route.profile}.`);
4123
+ }
4124
+ routes.set(route.profile, route);
4125
+ }
4126
+ return routes;
4127
+ };
4128
+ var createHarnessRouteModels = (routes, env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => Object.fromEntries([...routes].map(([profile, route]) => [
4129
+ profile,
4130
+ registry.createModel(route, env)
4131
+ ]));
4132
+ var serializeHarnessModelRoutes = (routes) => Object.fromEntries([...routes].map(([profile, route]) => [
4133
+ profile,
4134
+ { provider: route.provider, model: route.model }
4135
+ ]));
4136
+
4137
+ // src/cli-stream.ts
4138
+ var CLI_JSON_SCHEMA_VERSION = 1;
4139
+ var CLI_EVENT_SCHEMA_VERSION = 1;
4140
+ var streamEventDocument = (event, sequence = 0) => {
4141
+ if (!Number.isSafeInteger(sequence) || sequence < 0) {
4142
+ throw new Error("JSONL event sequence must be a non-negative integer.");
4143
+ }
4144
+ const base = {
4145
+ schemaVersion: CLI_EVENT_SCHEMA_VERSION,
4146
+ kind: "run-event",
4147
+ sequence,
4148
+ type: event.type
4149
+ };
4150
+ switch (event.type) {
4151
+ case "text-delta":
4152
+ return { ...base, textDelta: event.textDelta };
4153
+ case "tool-call":
4154
+ return { ...base, toolCallId: event.toolCall.id, toolName: event.toolCall.name };
4155
+ case "tool-result":
4156
+ return {
4157
+ ...base,
4158
+ toolCallId: event.toolResult.toolCallId,
4159
+ toolName: event.toolResult.toolName,
4160
+ isError: event.toolResult.isError
4161
+ };
4162
+ case "tool-approval-request":
4163
+ case "agent-approval-request":
4164
+ return {
4165
+ ...base,
4166
+ approvalRequestId: event.approval.id,
4167
+ approvalKind: event.approval.kind ?? "provider",
4168
+ toolName: event.approval.name
4169
+ };
4170
+ case "agent-approval-resolved":
4171
+ return {
4172
+ ...base,
4173
+ approvalRequestId: event.approval.approvalRequestId,
4174
+ approved: event.approval.approve
4175
+ };
4176
+ case "provider-data":
4177
+ return { ...base, provider: event.provider };
4178
+ case "image-generation":
4179
+ return {
4180
+ ...base,
4181
+ provider: event.provider,
4182
+ partial: event.partial,
4183
+ ...event.id ? { id: event.id } : {},
4184
+ ...event.index !== undefined ? { index: event.index } : {}
4185
+ };
4186
+ case "finish":
4187
+ return {
4188
+ ...base,
4189
+ ...event.finishReason ? { finishReason: event.finishReason } : {},
4190
+ ...event.usage ? { usage: event.usage } : {}
4191
+ };
4192
+ case "error":
4193
+ return { ...base, error: "Provider stream failed." };
4194
+ case "agent-run-start":
4195
+ return { ...base, currentStep: event.currentStep, maxSteps: event.maxSteps };
4196
+ case "agent-step-start":
4197
+ return { ...base, stepIndex: event.stepIndex };
4198
+ case "agent-step-finish":
4199
+ return {
4200
+ ...base,
4201
+ stepIndex: event.step.index,
4202
+ status: event.step.status,
4203
+ toolCalls: event.step.toolResults.length
4204
+ };
4205
+ case "agent-compaction":
4206
+ return {
4207
+ ...base,
4208
+ compactionId: event.compaction.id,
4209
+ reasons: event.compaction.reasons,
4210
+ messageCountBefore: event.compaction.messageCountBefore,
4211
+ messageCountAfter: event.compaction.messageCountAfter
4212
+ };
4213
+ case "agent-run-finish":
4214
+ return {
4215
+ ...base,
4216
+ runId: event.state.runId,
4217
+ status: event.status,
4218
+ provider: event.state.provider,
4219
+ model: event.state.modelId
4220
+ };
4221
+ }
4222
+ };
4223
+ var serializeStreamEvent = (event, sequence = 0) => JSON.stringify(streamEventDocument(event, sequence));
4224
+ var streamResultDocument = (result, sequence = 0) => {
4225
+ if (!Number.isSafeInteger(sequence) || sequence < 0) {
4226
+ throw new Error("JSONL result sequence must be a non-negative integer.");
4227
+ }
4228
+ return {
4229
+ schemaVersion: CLI_EVENT_SCHEMA_VERSION,
4230
+ kind: "run-result",
4231
+ sequence,
4232
+ runId: result.runId,
4233
+ status: result.status,
4234
+ provider: result.provider,
4235
+ model: result.model,
4236
+ steps: result.steps,
4237
+ toolCalls: result.toolCalls,
4238
+ pendingApprovals: (result.pendingApprovals ?? []).map((approval) => ({
4239
+ id: approval.id,
4240
+ kind: approval.kind,
4241
+ name: approval.name,
4242
+ ...approval.childRunId ? { childRunId: approval.childRunId } : {},
4243
+ ...approval.childAgentId ? { childAgentId: approval.childAgentId } : {}
4244
+ })),
4245
+ children: (result.children ?? []).map((child) => ({
4246
+ runId: child.runId,
4247
+ status: child.status
4248
+ }))
4249
+ };
4250
+ };
4251
+ var serializeStreamResult = (result, sequence = 0) => JSON.stringify(streamResultDocument(result, sequence));
4252
+
4253
+ // src/sessions.ts
4254
+ import { createHash as createHash8, randomUUID as randomUUID4 } from "crypto";
4255
+ import { chmod as chmod3, lstat as lstat6, mkdir as mkdir4, open as open3, realpath as realpath4, stat as stat3 } from "fs/promises";
4256
+ import path8 from "path";
4257
+ import { createRedactionPolicy as createRedactionPolicy3 } from "@zhivex-ai/agents";
4258
+ import { Database as Database2 } from "bun:sqlite";
4259
+ var HARNESS_SESSION_INDEX_FILE = "operations.sqlite";
4260
+ var HARNESS_SESSION_SCHEMA_VERSION = 1;
4261
+ var SESSION_RUN_STATUSES = [
4262
+ "created",
4263
+ "running",
4264
+ "waiting_approval",
4265
+ "cancel_requested",
4266
+ "completed",
4267
+ "failed",
4268
+ "cancelled",
4269
+ "timed_out"
4270
+ ];
4271
+ var TERMINAL_SESSION_RUN_STATUSES = [
4272
+ "completed",
4273
+ "failed",
4274
+ "cancelled",
4275
+ "timed_out"
4276
+ ];
4277
+ var DEFAULT_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
4278
+ var DEFAULT_MAX_SESSIONS = 200;
4279
+ var DEFAULT_MAX_RUNS = 1000;
4280
+ var DEFAULT_MAX_METADATA_BYTES = 4096;
4281
+ var DEFAULT_MAX_INDEX_BYTES = 16 * 1024 * 1024;
4282
+ var MAX_LIST_LIMIT = 200;
4283
+ var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,255}$/;
4284
+ var TERMINAL_STATUSES2 = new Set(TERMINAL_SESSION_RUN_STATUSES);
4285
+ var ACTIVE_STATUSES = new Set([
4286
+ "created",
4287
+ "running",
4288
+ "waiting_approval",
4289
+ "cancel_requested"
4290
+ ]);
4291
+ var STATUS_TRANSITIONS = {
4292
+ created: new Set(["created", "running", "waiting_approval", "completed", "failed", "cancelled", "timed_out"]),
4293
+ running: new Set(["running", "waiting_approval", "cancel_requested", "completed", "failed", "cancelled", "timed_out"]),
4294
+ waiting_approval: new Set(["waiting_approval", "running", "cancel_requested", "completed", "failed", "cancelled", "timed_out"]),
4295
+ cancel_requested: new Set(["cancel_requested", "cancelled", "failed", "timed_out"]),
4296
+ completed: new Set(["completed"]),
4297
+ failed: new Set(["failed"]),
4298
+ cancelled: new Set(["cancelled"]),
4299
+ timed_out: new Set(["timed_out"])
4300
+ };
4301
+ var metadataRedaction = createRedactionPolicy3({
4302
+ includeEmails: true,
4303
+ rules: [
4304
+ { name: "private-key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
4305
+ { name: "common-provider-key", pattern: /\b(?:sk|ghp|github_pat|xox[baprs])[-_][A-Za-z0-9_-]{8,}\b/gi },
4306
+ { name: "url-credentials", pattern: /:\/\/[^\s/@:]+:[^\s/@]+@/g, replacement: "://[REDACTED]@" }
4307
+ ]
4308
+ });
4309
+ var byteLength = (value) => Buffer.byteLength(value, "utf8");
4310
+ var boundedInteger = (name, value, fallback, minimum) => {
4311
+ const resolved = value ?? fallback;
4312
+ if (!Number.isSafeInteger(resolved) || resolved < minimum) {
4313
+ throw new Error(`${name} must be an integer greater than or equal to ${minimum}.`);
4314
+ }
4315
+ return resolved;
4316
+ };
4317
+ var assertTimestamp = (name, value) => {
4318
+ if (!Number.isSafeInteger(value) || value < 0) {
4319
+ throw new Error(`${name} must be a non-negative integer timestamp.`);
4320
+ }
4321
+ };
4322
+ var assertIdentifier = (name, value) => {
4323
+ if (!ID_PATTERN.test(value)) {
4324
+ throw new Error(`${name} contains unsupported characters or exceeds 256 bytes.`);
4325
+ }
4326
+ if (metadataRedaction.redactText(value) !== value) {
4327
+ throw new Error(`${name} resembles sensitive metadata and cannot be stored in the session index.`);
4328
+ }
4329
+ return value;
4330
+ };
4331
+ var assertScopeSegment = (name, value) => {
4332
+ const normalized = value.trim();
4333
+ if (!normalized || normalized.length > 128 || /[\u0000-\u001f\u007f]/.test(normalized)) {
4334
+ throw new Error(`${name} must contain 1-128 printable characters.`);
4335
+ }
4336
+ return normalized;
4337
+ };
4338
+ var normalizeTitle = (value, maxMetadataBytes) => {
4339
+ if (value === undefined || value === null) {
4340
+ return value;
4341
+ }
4342
+ const normalized = metadataRedaction.redactText(value).replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
4343
+ if (!normalized) {
4344
+ throw new Error("Session title must not be empty.");
4345
+ }
4346
+ if (byteLength(normalized) > maxMetadataBytes) {
4347
+ throw new Error(`Session title exceeds the ${maxMetadataBytes}-byte metadata limit.`);
4348
+ }
4349
+ return normalized;
4350
+ };
4351
+ var normalizeScope = (scope) => {
4352
+ const tenantId = assertScopeSegment("scope.tenantId", scope.tenantId);
4353
+ const userId = scope.userId === undefined ? "" : assertScopeSegment("scope.userId", scope.userId);
4354
+ const namespace = scope.namespace === undefined ? "" : assertScopeSegment("scope.namespace", scope.namespace);
4355
+ return `${tenantId}\x00${userId}\x00${namespace}`;
4356
+ };
4357
+ var stableKey = (kind, value) => createHash8("sha256").update(kind).update("\x00").update(value).digest("hex");
4358
+ var sessionId = () => `ses_${randomUUID4()}`;
4359
+ var turnId = () => `turn_${randomUUID4()}`;
4360
+ var isMissing = (error) => error.code === "ENOENT";
4361
+ var ensurePrivateDatabase = async (workspace, stateDirectory) => {
4362
+ const requestedStateDirectory = path8.resolve(stateDirectory);
4363
+ await validateStateDirectory(workspace, requestedStateDirectory);
4364
+ await mkdir4(requestedStateDirectory, { recursive: true, mode: 448 });
4365
+ const directoryEntry = await lstat6(requestedStateDirectory);
4366
+ if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
4367
+ throw new Error(`The session state directory must be a real directory: ${requestedStateDirectory}.`);
4368
+ }
4369
+ await chmod3(requestedStateDirectory, 448);
4370
+ const canonicalStateDirectory2 = await realpath4(requestedStateDirectory);
4371
+ const databasePath = path8.join(canonicalStateDirectory2, HARNESS_SESSION_INDEX_FILE);
4372
+ let databaseEntry;
4373
+ try {
4374
+ databaseEntry = await lstat6(databasePath);
4375
+ } catch (error) {
4376
+ if (!isMissing(error)) {
4377
+ throw error;
4378
+ }
4379
+ try {
4380
+ const handle = await open3(databasePath, "wx", 384);
4381
+ await handle.close();
4382
+ } catch (creationError) {
4383
+ if (creationError.code !== "EEXIST") {
4384
+ throw creationError;
4385
+ }
4386
+ }
4387
+ databaseEntry = await lstat6(databasePath);
4388
+ }
4389
+ if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
4390
+ throw new Error(`The session SQLite path must be a real file: ${databasePath}.`);
4391
+ }
4392
+ await chmod3(databasePath, 384);
4393
+ return { databasePath, databaseEntry };
4394
+ };
4395
+ var toRunReference = (row) => ({
4396
+ turnId: row.turn_id,
4397
+ ...row.source_turn_id ? { sourceTurnId: row.source_turn_id } : {},
4398
+ sequence: row.ordinal,
4399
+ runId: row.run_id,
4400
+ provider: row.provider,
4401
+ model: row.model,
4402
+ ...row.role ? { role: row.role } : {},
4403
+ status: row.status,
4404
+ createdAt: row.created_at,
4405
+ updatedAt: row.updated_at,
4406
+ ...row.completed_at === null ? {} : { completedAt: row.completed_at }
4407
+ });
4408
+ var metadataSize = (input) => Object.values(input).reduce((total, value) => total + (typeof value === "string" ? byteLength(value) : 0), 0);
4409
+ var isTerminal = (status) => TERMINAL_STATUSES2.has(status);
4410
+ var openCliSessionStore = async (options) => {
4411
+ const workspace = await realpath4(path8.resolve(options.workspace));
4412
+ const workspaceEntry = await stat3(workspace);
4413
+ if (!workspaceEntry.isDirectory()) {
4414
+ throw new Error(`The session workspace is not a directory: ${workspace}.`);
4415
+ }
4416
+ const scopeValue2 = normalizeScope(options.scope);
4417
+ const workspaceKey = stableKey("workspace", workspace);
4418
+ const scopeKey = stableKey("scope", scopeValue2);
4419
+ const retentionMs = boundedInteger("retentionMs", options.retentionMs, DEFAULT_RETENTION_MS, 0);
4420
+ const maxSessions = boundedInteger("maxSessionsPerWorkspace", options.maxSessionsPerWorkspace, DEFAULT_MAX_SESSIONS, 1);
4421
+ const maxRuns = boundedInteger("maxRunsPerSession", options.maxRunsPerSession, DEFAULT_MAX_RUNS, 1);
4422
+ const maxMetadataBytes = boundedInteger("maxMetadataBytes", options.maxMetadataBytes, DEFAULT_MAX_METADATA_BYTES, 32);
4423
+ const maxIndexBytes = boundedInteger("maxIndexBytes", options.maxIndexBytes, DEFAULT_MAX_INDEX_BYTES, 1024);
4424
+ const now = options.now ?? Date.now;
4425
+ const { databasePath, databaseEntry } = await ensurePrivateDatabase(workspace, options.stateDirectory);
4426
+ const database = new Database2(databasePath, { create: false, strict: true });
4427
+ let closed = false;
4428
+ const assertOpen = () => {
4429
+ if (closed) {
4430
+ throw new Error("The CLI session store is closed.");
4431
+ }
4432
+ };
4433
+ const transaction = (operation) => {
4434
+ assertOpen();
4435
+ database.exec("BEGIN IMMEDIATE");
4436
+ try {
4437
+ const result = operation();
4438
+ const size = database.query(`
4439
+ SELECT
4440
+ COALESCE((SELECT SUM(metadata_bytes) FROM zhivex_cli_sessions WHERE workspace_key = ?1 AND scope_key = ?2), 0) +
4441
+ COALESCE((SELECT SUM(r.metadata_bytes) FROM zhivex_cli_session_runs r JOIN zhivex_cli_sessions s ON s.session_id = r.session_id WHERE s.workspace_key = ?1 AND s.scope_key = ?2), 0)
4442
+ AS size
4443
+ `).get(workspaceKey, scopeKey)?.size ?? 0;
4444
+ if (size > maxIndexBytes) {
4445
+ throw new Error(`Session index metadata exceeds the ${maxIndexBytes}-byte limit.`);
4446
+ }
4447
+ database.exec("COMMIT");
4448
+ return result;
4449
+ } catch (error) {
4450
+ try {
4451
+ database.exec("ROLLBACK");
4452
+ } catch {}
4453
+ throw error;
4454
+ }
4455
+ };
4456
+ const ensureRevision = (row, expected) => {
4457
+ if (expected !== undefined && row.revision !== expected) {
4458
+ throw new Error(`Session ${row.session_id} changed concurrently (expected revision ${expected}, found ${row.revision}).`);
4459
+ }
4460
+ };
4461
+ const selectSessionRow = (candidateSessionId, includeDeleted = false) => {
4462
+ assertIdentifier("sessionId", candidateSessionId);
4463
+ return database.query(`
4464
+ SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
4465
+ created_at, updated_at, archived_at, deleted_at
4466
+ FROM zhivex_cli_sessions
4467
+ WHERE session_id = ?1 AND workspace_key = ?2 AND scope_key = ?3
4468
+ ${includeDeleted ? "" : "AND deleted_at IS NULL"}
4469
+ `).get(candidateSessionId, workspaceKey, scopeKey);
4470
+ };
4471
+ const requireSessionRow = (candidateSessionId, includeDeleted = false) => {
4472
+ const row = selectSessionRow(candidateSessionId, includeDeleted);
4473
+ if (!row) {
4474
+ throw new Error(`Session ${candidateSessionId} was not found in this workspace and scope.`);
4475
+ }
4476
+ return row;
4477
+ };
4478
+ const selectRuns = (candidateSessionId) => database.query(`
4479
+ SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
4480
+ created_at, updated_at, completed_at
4481
+ FROM zhivex_cli_session_runs
4482
+ WHERE session_id = ?1
4483
+ ORDER BY ordinal ASC
4484
+ `).all(candidateSessionId).map(toRunReference);
4485
+ const nextActivitySequence = () => database.query(`
4486
+ SELECT COALESCE(MAX(activity_seq), 0) + 1 AS count
4487
+ FROM zhivex_cli_sessions
4488
+ WHERE workspace_key = ?1 AND scope_key = ?2
4489
+ `).get(workspaceKey, scopeKey)?.count ?? 1;
4490
+ const sessionBase = (row) => ({
4491
+ schemaVersion: HARNESS_SESSION_SCHEMA_VERSION,
4492
+ sessionId: row.session_id,
4493
+ workspaceKey,
4494
+ scopeKey,
4495
+ ...row.title ? { title: row.title } : {},
4496
+ ...row.parent_session_id ? { parentSessionId: row.parent_session_id } : {},
4497
+ ...row.forked_from_turn_id ? { forkedFromTurnId: row.forked_from_turn_id } : {},
4498
+ revision: row.revision,
4499
+ createdAt: row.created_at,
4500
+ updatedAt: row.updated_at,
4501
+ ...row.archived_at === null ? {} : { archivedAt: row.archived_at },
4502
+ ...row.deleted_at === null ? {} : { deletedAt: row.deleted_at }
4503
+ });
4504
+ const hydrate = (row) => ({
4505
+ ...sessionBase(row),
4506
+ runs: selectRuns(row.session_id)
4507
+ });
4508
+ const latestRunRow = (candidateSessionId) => database.query(`
4509
+ SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
4510
+ created_at, updated_at, completed_at
4511
+ FROM zhivex_cli_session_runs
4512
+ WHERE session_id = ?1
4513
+ ORDER BY ordinal DESC
4514
+ LIMIT 1
4515
+ `).get(candidateSessionId);
4516
+ const assertNoActiveRun = (candidateSessionId, action) => {
4517
+ const latest = latestRunRow(candidateSessionId);
4518
+ if (latest && ACTIVE_STATUSES.has(latest.status)) {
4519
+ throw new Error(`Cannot ${action} session ${candidateSessionId} while run ${latest.run_id} is ${latest.status}.`);
4520
+ }
4521
+ };
4522
+ const insertRun = (candidateSessionId, input, sequence, sourceTurnId) => {
4523
+ const runId = assertIdentifier("runId", input.runId);
4524
+ const provider = assertIdentifier("provider", input.provider);
4525
+ const model = assertIdentifier("model", input.model);
4526
+ const role = input.role === undefined ? null : assertIdentifier("role", input.role);
4527
+ const status = input.status ?? "created";
4528
+ if (!SESSION_RUN_STATUSES.includes(status)) {
4529
+ throw new Error(`Unsupported session run status: ${String(status)}.`);
4530
+ }
4531
+ const createdAt = input.createdAt ?? now();
4532
+ assertTimestamp("createdAt", createdAt);
4533
+ const entryBytes = metadataSize({ runId, provider, model, role });
4534
+ if (entryBytes > maxMetadataBytes) {
4535
+ throw new Error(`Run reference metadata exceeds the ${maxMetadataBytes}-byte limit.`);
4536
+ }
4537
+ const candidateTurnId = turnId();
4538
+ database.query(`
4539
+ INSERT INTO zhivex_cli_session_runs (
4540
+ turn_id, source_turn_id, session_id, ordinal, run_id, provider, model, role,
4541
+ status, created_at, updated_at, completed_at, metadata_bytes
4542
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10, ?11, ?12)
4543
+ `).run(candidateTurnId, sourceTurnId ?? null, candidateSessionId, sequence, runId, provider, model, role, status, createdAt, isTerminal(status) ? createdAt : null, entryBytes);
4544
+ };
4545
+ try {
4546
+ const openedEntry = await lstat6(databasePath);
4547
+ if (openedEntry.isSymbolicLink() || !openedEntry.isFile() || openedEntry.dev !== databaseEntry.dev || openedEntry.ino !== databaseEntry.ino) {
4548
+ throw new Error(`The session SQLite path changed while it was being opened: ${databasePath}.`);
4549
+ }
4550
+ database.exec("PRAGMA journal_mode = WAL");
4551
+ database.exec("PRAGMA busy_timeout = 5000");
4552
+ database.exec("PRAGMA foreign_keys = ON");
4553
+ database.exec(`
4554
+ CREATE TABLE IF NOT EXISTS zhivex_cli_session_schema (
4555
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
4556
+ version INTEGER NOT NULL
4557
+ );
4558
+ INSERT OR IGNORE INTO zhivex_cli_session_schema (singleton, version)
4559
+ VALUES (1, ${HARNESS_SESSION_SCHEMA_VERSION});
4560
+ CREATE TABLE IF NOT EXISTS zhivex_cli_sessions (
4561
+ session_id TEXT PRIMARY KEY,
4562
+ workspace_key TEXT NOT NULL,
4563
+ scope_key TEXT NOT NULL,
4564
+ title TEXT,
4565
+ parent_session_id TEXT,
4566
+ forked_from_turn_id TEXT,
4567
+ revision INTEGER NOT NULL DEFAULT 0,
4568
+ activity_seq INTEGER NOT NULL,
4569
+ created_at INTEGER NOT NULL,
4570
+ updated_at INTEGER NOT NULL,
4571
+ archived_at INTEGER,
4572
+ deleted_at INTEGER,
4573
+ metadata_bytes INTEGER NOT NULL,
4574
+ CHECK (length(session_id) <= 260),
4575
+ CHECK (metadata_bytes >= 0)
4576
+ );
4577
+ CREATE INDEX IF NOT EXISTS zhivex_cli_sessions_latest
4578
+ ON zhivex_cli_sessions (workspace_key, scope_key, deleted_at, archived_at, activity_seq DESC);
4579
+ CREATE TABLE IF NOT EXISTS zhivex_cli_session_runs (
4580
+ turn_id TEXT PRIMARY KEY,
4581
+ source_turn_id TEXT,
4582
+ session_id TEXT NOT NULL REFERENCES zhivex_cli_sessions(session_id) ON DELETE CASCADE,
4583
+ ordinal INTEGER NOT NULL,
4584
+ run_id TEXT NOT NULL,
4585
+ provider TEXT NOT NULL,
4586
+ model TEXT NOT NULL,
4587
+ role TEXT,
4588
+ status TEXT NOT NULL CHECK (status IN ('created', 'running', 'waiting_approval', 'cancel_requested', 'completed', 'failed', 'cancelled', 'timed_out')),
4589
+ created_at INTEGER NOT NULL,
4590
+ updated_at INTEGER NOT NULL,
4591
+ completed_at INTEGER,
4592
+ metadata_bytes INTEGER NOT NULL,
4593
+ UNIQUE (session_id, ordinal),
4594
+ UNIQUE (session_id, run_id),
4595
+ CHECK (ordinal >= 0),
4596
+ CHECK (metadata_bytes >= 0)
4597
+ );
4598
+ CREATE INDEX IF NOT EXISTS zhivex_cli_session_runs_latest
4599
+ ON zhivex_cli_session_runs (session_id, ordinal DESC);
4600
+ `);
4601
+ const schemaVersion = database.query("SELECT version FROM zhivex_cli_session_schema WHERE singleton = 1").get()?.version;
4602
+ if (schemaVersion !== HARNESS_SESSION_SCHEMA_VERSION) {
4603
+ throw new Error(`Unsupported CLI session schema version: ${String(schemaVersion)}.`);
4604
+ }
4605
+ await chmod3(databasePath, 384);
4606
+ } catch (error) {
4607
+ database.close(false);
4608
+ throw error;
4609
+ }
4610
+ const prune = async (input = {}) => transaction(() => {
4611
+ const before = input.before ?? Math.max(0, now() - retentionMs);
4612
+ assertTimestamp("before", before);
4613
+ const deletedSessions = database.query(`
4614
+ SELECT COUNT(*) AS count FROM zhivex_cli_sessions
4615
+ WHERE workspace_key = ?1 AND scope_key = ?2
4616
+ AND updated_at < ?3
4617
+ AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
4618
+ `).get(workspaceKey, scopeKey, before)?.count ?? 0;
4619
+ database.query(`
4620
+ DELETE FROM zhivex_cli_sessions
4621
+ WHERE workspace_key = ?1 AND scope_key = ?2
4622
+ AND updated_at < ?3
4623
+ AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
4624
+ `).run(workspaceKey, scopeKey, before);
4625
+ return { before, deletedSessions };
4626
+ });
4627
+ const store = {
4628
+ databasePath,
4629
+ workspaceKey,
4630
+ scopeKey,
4631
+ async create(input = {}) {
4632
+ return transaction(() => {
4633
+ const cutoff = Math.max(0, now() - retentionMs);
4634
+ database.query(`
4635
+ DELETE FROM zhivex_cli_sessions
4636
+ WHERE workspace_key = ?1 AND scope_key = ?2 AND updated_at < ?3
4637
+ AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
4638
+ `).run(workspaceKey, scopeKey, cutoff);
4639
+ const count = database.query(`
4640
+ SELECT COUNT(*) AS count FROM zhivex_cli_sessions
4641
+ WHERE workspace_key = ?1 AND scope_key = ?2
4642
+ `).get(workspaceKey, scopeKey)?.count ?? 0;
4643
+ if (count >= maxSessions) {
4644
+ throw new Error(`Session limit reached for this workspace and scope (${maxSessions}). Archive and prune an older session.`);
4645
+ }
4646
+ const createdAt = now();
4647
+ assertTimestamp("now", createdAt);
4648
+ const title = normalizeTitle(input.title, maxMetadataBytes) ?? null;
4649
+ const candidateSessionId = sessionId();
4650
+ const activitySequence = nextActivitySequence();
4651
+ database.query(`
4652
+ INSERT INTO zhivex_cli_sessions (
4653
+ session_id, workspace_key, scope_key, title, revision, created_at,
4654
+ updated_at, activity_seq, metadata_bytes
4655
+ ) VALUES (?1, ?2, ?3, ?4, 0, ?5, ?5, ?6, ?7)
4656
+ `).run(candidateSessionId, workspaceKey, scopeKey, title, createdAt, activitySequence, metadataSize({ title }));
4657
+ if (input.initialRun) {
4658
+ insertRun(candidateSessionId, input.initialRun, 0);
4659
+ }
4660
+ return hydrate(requireSessionRow(candidateSessionId));
4661
+ });
4662
+ },
4663
+ async get(candidateSessionId, input = {}) {
4664
+ assertOpen();
4665
+ const row = selectSessionRow(candidateSessionId, input.includeDeleted ?? false);
4666
+ return row ? hydrate(row) : undefined;
4667
+ },
4668
+ async list(query = {}) {
4669
+ assertOpen();
4670
+ const limit = boundedInteger("limit", query.limit, 50, 1);
4671
+ if (limit > MAX_LIST_LIMIT) {
4672
+ throw new Error(`limit cannot exceed ${MAX_LIST_LIMIT}.`);
4673
+ }
4674
+ const rows = database.query(`
4675
+ SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
4676
+ created_at, updated_at, archived_at, deleted_at
4677
+ FROM zhivex_cli_sessions
4678
+ WHERE workspace_key = ?1 AND scope_key = ?2
4679
+ ${query.includeArchived ? "" : "AND archived_at IS NULL"}
4680
+ ${query.includeDeleted ? "" : "AND deleted_at IS NULL"}
4681
+ ORDER BY activity_seq DESC
4682
+ LIMIT ?3
4683
+ `).all(workspaceKey, scopeKey, limit);
4684
+ return rows.map((row) => {
4685
+ const latest = latestRunRow(row.session_id);
4686
+ const runCount = database.query("SELECT COUNT(*) AS count FROM zhivex_cli_session_runs WHERE session_id = ?1").get(row.session_id)?.count ?? 0;
4687
+ return {
4688
+ ...sessionBase(row),
4689
+ runCount,
4690
+ ...latest ? { latestRun: toRunReference(latest) } : {}
4691
+ };
4692
+ });
4693
+ },
4694
+ async latest(input = {}) {
4695
+ assertOpen();
4696
+ const row = database.query(`
4697
+ SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
4698
+ created_at, updated_at, archived_at, deleted_at
4699
+ FROM zhivex_cli_sessions
4700
+ WHERE workspace_key = ?1 AND scope_key = ?2 AND deleted_at IS NULL
4701
+ ${input.includeArchived ? "" : "AND archived_at IS NULL"}
4702
+ ORDER BY activity_seq DESC
4703
+ LIMIT 1
4704
+ `).get(workspaceKey, scopeKey);
4705
+ return row ? hydrate(row) : undefined;
4706
+ },
4707
+ async update(candidateSessionId, input) {
4708
+ return transaction(() => {
4709
+ const row = requireSessionRow(candidateSessionId);
4710
+ ensureRevision(row, input.expectedRevision);
4711
+ if (input.archived === true) {
4712
+ assertNoActiveRun(candidateSessionId, "archive");
4713
+ }
4714
+ const title = input.title === undefined ? row.title : normalizeTitle(input.title, maxMetadataBytes) ?? null;
4715
+ const updatedAt = now();
4716
+ assertTimestamp("now", updatedAt);
4717
+ const archivedAt = input.archived === undefined ? row.archived_at : input.archived ? updatedAt : null;
4718
+ const activitySequence = nextActivitySequence();
4719
+ database.query(`
4720
+ UPDATE zhivex_cli_sessions
4721
+ SET title = ?1, archived_at = ?2, updated_at = ?3, revision = revision + 1,
4722
+ metadata_bytes = ?4, activity_seq = ?5
4723
+ WHERE session_id = ?6 AND workspace_key = ?7 AND scope_key = ?8 AND revision = ?9
4724
+ `).run(title, archivedAt, updatedAt, metadataSize({ title }), activitySequence, candidateSessionId, workspaceKey, scopeKey, row.revision);
4725
+ return hydrate(requireSessionRow(candidateSessionId));
4726
+ });
4727
+ },
4728
+ async rename(candidateSessionId, title, input = {}) {
4729
+ return store.update(candidateSessionId, {
4730
+ title,
4731
+ ...input.expectedRevision === undefined ? {} : { expectedRevision: input.expectedRevision }
4732
+ });
4733
+ },
4734
+ async appendRun(candidateSessionId, input, appendOptions = {}) {
4735
+ return transaction(() => {
4736
+ const row = requireSessionRow(candidateSessionId);
4737
+ ensureRevision(row, appendOptions.expectedRevision);
4738
+ if (row.archived_at !== null) {
4739
+ throw new Error(`Cannot append a run to archived session ${candidateSessionId}.`);
4740
+ }
4741
+ const existing = database.query(`
4742
+ SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
4743
+ created_at, updated_at, completed_at
4744
+ FROM zhivex_cli_session_runs WHERE session_id = ?1 AND run_id = ?2
4745
+ `).get(candidateSessionId, input.runId);
4746
+ if (existing) {
4747
+ const requestedStatus = input.status ?? "created";
4748
+ if (existing.provider === input.provider && existing.model === input.model && (existing.role ?? undefined) === input.role && existing.status === requestedStatus) {
4749
+ return hydrate(row);
4750
+ }
4751
+ throw new Error(`Run ${input.runId} is already immutably bound to ${existing.provider}/${existing.model} in this session.`);
4752
+ }
4753
+ assertNoActiveRun(candidateSessionId, "start another run in");
4754
+ const count = database.query("SELECT COUNT(*) AS count FROM zhivex_cli_session_runs WHERE session_id = ?1").get(candidateSessionId)?.count ?? 0;
4755
+ if (count >= maxRuns) {
4756
+ throw new Error(`Run-reference limit reached for session ${candidateSessionId} (${maxRuns}).`);
4757
+ }
4758
+ insertRun(candidateSessionId, input, count);
4759
+ const updatedAt = now();
4760
+ assertTimestamp("now", updatedAt);
4761
+ const activitySequence = nextActivitySequence();
4762
+ database.query(`
4763
+ UPDATE zhivex_cli_sessions SET updated_at = ?1, activity_seq = ?2, revision = revision + 1
4764
+ WHERE session_id = ?3 AND revision = ?4
4765
+ `).run(updatedAt, activitySequence, candidateSessionId, row.revision);
4766
+ return hydrate(requireSessionRow(candidateSessionId));
4767
+ });
4768
+ },
4769
+ async updateRun(candidateSessionId, runId, input) {
4770
+ return transaction(() => {
4771
+ const session = requireSessionRow(candidateSessionId);
4772
+ ensureRevision(session, input.expectedSessionRevision);
4773
+ assertIdentifier("runId", runId);
4774
+ const run = database.query(`
4775
+ SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
4776
+ created_at, updated_at, completed_at
4777
+ FROM zhivex_cli_session_runs WHERE session_id = ?1 AND run_id = ?2
4778
+ `).get(candidateSessionId, runId);
4779
+ if (!run) {
4780
+ throw new Error(`Run ${runId} is not linked to session ${candidateSessionId}.`);
4781
+ }
4782
+ if (!STATUS_TRANSITIONS[run.status].has(input.status)) {
4783
+ throw new Error(`Run ${runId} cannot transition from ${run.status} to ${input.status}.`);
4784
+ }
4785
+ const updatedAt = input.updatedAt ?? now();
4786
+ assertTimestamp("updatedAt", updatedAt);
4787
+ if (updatedAt < run.updated_at) {
4788
+ throw new Error(`Run ${runId} update timestamp cannot move backwards.`);
4789
+ }
4790
+ const completedAt = isTerminal(input.status) ? input.completedAt ?? updatedAt : null;
4791
+ if (completedAt !== null) {
4792
+ assertTimestamp("completedAt", completedAt);
4793
+ if (completedAt < run.created_at || completedAt > updatedAt) {
4794
+ throw new Error("completedAt must be between the run creation and update timestamps.");
4795
+ }
4796
+ }
4797
+ database.query(`
4798
+ UPDATE zhivex_cli_session_runs
4799
+ SET status = ?1, updated_at = ?2, completed_at = ?3
4800
+ WHERE session_id = ?4 AND run_id = ?5
4801
+ `).run(input.status, updatedAt, completedAt, candidateSessionId, runId);
4802
+ const activitySequence = nextActivitySequence();
4803
+ database.query(`
4804
+ UPDATE zhivex_cli_sessions SET updated_at = ?1, activity_seq = ?2, revision = revision + 1
4805
+ WHERE session_id = ?3 AND revision = ?4
4806
+ `).run(updatedAt, activitySequence, candidateSessionId, session.revision);
4807
+ return hydrate(requireSessionRow(candidateSessionId));
4808
+ });
4809
+ },
4810
+ async fork(candidateSessionId, input = {}) {
4811
+ return transaction(() => {
4812
+ const source = requireSessionRow(candidateSessionId);
4813
+ ensureRevision(source, input.expectedRevision);
4814
+ const sourceRuns = selectRuns(candidateSessionId);
4815
+ let forkRuns = sourceRuns;
4816
+ let forkedFromTurnId;
4817
+ if (input.atTurnId) {
4818
+ const index = sourceRuns.findIndex((run) => run.turnId === input.atTurnId);
4819
+ if (index < 0) {
4820
+ throw new Error(`Turn ${input.atTurnId} is not part of session ${candidateSessionId}.`);
4821
+ }
4822
+ forkRuns = sourceRuns.slice(0, index + 1);
4823
+ forkedFromTurnId = input.atTurnId;
4824
+ } else if (sourceRuns.length > 0) {
4825
+ forkedFromTurnId = sourceRuns.at(-1).turnId;
4826
+ }
4827
+ const branchPoint = forkRuns.at(-1);
4828
+ if (branchPoint && !isTerminal(branchPoint.status)) {
4829
+ throw new Error(`Cannot fork from non-terminal run ${branchPoint.runId} (${branchPoint.status}).`);
4830
+ }
4831
+ if (forkRuns.length > maxRuns) {
4832
+ throw new Error(`Fork history exceeds the ${maxRuns}-run limit.`);
4833
+ }
4834
+ const count = database.query(`
4835
+ SELECT COUNT(*) AS count FROM zhivex_cli_sessions
4836
+ WHERE workspace_key = ?1 AND scope_key = ?2
4837
+ `).get(workspaceKey, scopeKey)?.count ?? 0;
4838
+ if (count >= maxSessions) {
4839
+ throw new Error(`Session limit reached for this workspace and scope (${maxSessions}).`);
4840
+ }
4841
+ const createdAt = now();
4842
+ assertTimestamp("now", createdAt);
4843
+ const title = normalizeTitle(input.title, maxMetadataBytes) ?? source.title;
4844
+ const forkSessionId = sessionId();
4845
+ const activitySequence = nextActivitySequence();
4846
+ database.query(`
4847
+ INSERT INTO zhivex_cli_sessions (
4848
+ session_id, workspace_key, scope_key, title, parent_session_id,
4849
+ forked_from_turn_id, revision, created_at, updated_at, activity_seq, metadata_bytes
4850
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7, ?7, ?8, ?9)
4851
+ `).run(forkSessionId, workspaceKey, scopeKey, title, candidateSessionId, forkedFromTurnId ?? null, createdAt, activitySequence, metadataSize({ title }));
4852
+ for (const [index, run] of forkRuns.entries()) {
4853
+ insertRun(forkSessionId, {
4854
+ runId: run.runId,
4855
+ provider: run.provider,
4856
+ model: run.model,
4857
+ ...run.role ? { role: run.role } : {},
4858
+ status: run.status,
4859
+ createdAt: run.createdAt
4860
+ }, index, run.turnId);
4861
+ database.query(`
4862
+ UPDATE zhivex_cli_session_runs
4863
+ SET updated_at = ?1, completed_at = ?2
4864
+ WHERE session_id = ?3 AND ordinal = ?4
4865
+ `).run(run.updatedAt, run.completedAt ?? null, forkSessionId, index);
4866
+ }
4867
+ return hydrate(requireSessionRow(forkSessionId));
4868
+ });
4869
+ },
4870
+ async archive(candidateSessionId, input = {}) {
4871
+ return store.update(candidateSessionId, {
4872
+ archived: true,
4873
+ ...input.expectedRevision === undefined ? {} : { expectedRevision: input.expectedRevision }
4874
+ });
4875
+ },
4876
+ async delete(candidateSessionId, input = {}) {
4877
+ return transaction(() => {
4878
+ const row = requireSessionRow(candidateSessionId);
4879
+ ensureRevision(row, input.expectedRevision);
4880
+ assertNoActiveRun(candidateSessionId, "delete");
4881
+ const deletedAt = now();
4882
+ assertTimestamp("now", deletedAt);
4883
+ const activitySequence = nextActivitySequence();
4884
+ database.query(`
4885
+ UPDATE zhivex_cli_sessions
4886
+ SET deleted_at = ?1, updated_at = ?1, activity_seq = ?2, revision = revision + 1
4887
+ WHERE session_id = ?3 AND revision = ?4
4888
+ `).run(deletedAt, activitySequence, candidateSessionId, row.revision);
4889
+ return hydrate(requireSessionRow(candidateSessionId, true));
4890
+ });
4891
+ },
4892
+ prune,
4893
+ close() {
4894
+ if (!closed) {
4895
+ closed = true;
4896
+ database.close(false);
4897
+ }
4898
+ }
4899
+ };
4900
+ return store;
4901
+ };
4902
+ var openSessionStore = openCliSessionStore;
4903
+
4904
+ // src/cli.ts
4905
+ var HARNESS_RESUME_METADATA_KEY = "zhivexHarnessResume";
4906
+ var HARNESS_RESUME_METADATA_SCHEMA_VERSION = 1;
4907
+ var CLI_EXIT_CODES = {
4908
+ success: 0,
4909
+ runtimeError: 1,
4910
+ usageError: 2,
4911
+ doctorFailed: 3
4912
+ };
4913
+ var COMMANDS = new Set(["run", "review", "chat", "providers", "doctor", "resume", "runs", "sessions", "help", "version"]);
4914
+ var RUNS_COMMANDS = new Set(["list", "inspect", "cancel", "cleanup", "export"]);
4915
+ var SESSIONS_COMMANDS = new Set(["list", "inspect", "rename", "fork", "archive"]);
4916
+ var RUN_STATUSES = new Set([
4917
+ "queued",
4918
+ "running",
4919
+ "completed",
4920
+ "suspended",
4921
+ "waiting_approval",
4922
+ "cancel_requested",
4923
+ "failed",
4924
+ "cancelled",
4925
+ "timed_out"
4926
+ ]);
4927
+
4928
+ class CliUsageError extends Error {
4929
+ constructor(message) {
4930
+ super(message);
4931
+ this.name = "CliUsageError";
3892
4932
  }
3893
4933
  }
3894
4934
  var harnessConfigInput = (config) => ({
@@ -3943,13 +4983,14 @@ var harnessConfigInput = (config) => ({
3943
4983
  } : {},
3944
4984
  ...config.mcpConfigPath ? { mcpConfigPath: config.mcpConfigPath } : {}
3945
4985
  });
3946
- var createHarnessResumeMetadata = (config) => ({
4986
+ var createHarnessResumeMetadata = (config, routes) => ({
3947
4987
  [HARNESS_RESUME_METADATA_KEY]: JSON.parse(JSON.stringify({
3948
4988
  schemaVersion: HARNESS_RESUME_METADATA_SCHEMA_VERSION,
3949
- config
4989
+ config,
4990
+ ...routes && routes.size > 0 ? { routes: serializeHarnessModelRoutes(routes) } : {}
3950
4991
  }))
3951
4992
  });
3952
- var readHarnessResumeConfig = (state) => {
4993
+ var readHarnessResumeDocument = (state) => {
3953
4994
  const value = state.metadata?.[HARNESS_RESUME_METADATA_KEY];
3954
4995
  if (value === undefined)
3955
4996
  return;
@@ -3960,12 +5001,39 @@ var readHarnessResumeConfig = (state) => {
3960
5001
  if (document.schemaVersion !== HARNESS_RESUME_METADATA_SCHEMA_VERSION || !document.config || typeof document.config !== "object" || Array.isArray(document.config)) {
3961
5002
  throw new Error("Persisted harness resume metadata is invalid.");
3962
5003
  }
5004
+ return document;
5005
+ };
5006
+ var readHarnessResumeConfig = (state) => {
5007
+ const document = readHarnessResumeDocument(state);
5008
+ if (!document)
5009
+ return;
3963
5010
  try {
3964
5011
  return harnessConfigInput(document.config);
3965
5012
  } catch {
3966
5013
  throw new Error("Persisted harness resume metadata is invalid.");
3967
5014
  }
3968
5015
  };
5016
+ var readHarnessResumeRoutes = (state) => {
5017
+ const document = readHarnessResumeDocument(state);
5018
+ if (!document?.routes)
5019
+ return resolveHarnessModelRoutes();
5020
+ if (typeof document.routes !== "object" || Array.isArray(document.routes)) {
5021
+ throw new Error("Persisted harness routing metadata is invalid.");
5022
+ }
5023
+ try {
5024
+ const values = Object.entries(document.routes).map(([profile, target]) => {
5025
+ if (!target || typeof target !== "object" || Array.isArray(target))
5026
+ throw new Error("invalid");
5027
+ const route = target;
5028
+ if (typeof route.provider !== "string" || typeof route.model !== "string")
5029
+ throw new Error("invalid");
5030
+ return `${profile}=${route.provider}:${route.model}`;
5031
+ });
5032
+ return resolveHarnessModelRoutes(values);
5033
+ } catch {
5034
+ throw new Error("Persisted harness routing metadata is invalid.");
5035
+ }
5036
+ };
3969
5037
  var shellArgument = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
3970
5038
  var resumeCommand = (runId, config, approve = true) => [
3971
5039
  "zhivex-harness",
@@ -3994,7 +5062,16 @@ var parseCliArgs = (argv) => {
3994
5062
  let command = "run";
3995
5063
  let commandWasExplicit = false;
3996
5064
  const positional = [];
3997
- const options = { command, cascade: false, final: false, yes: false, json: false };
5065
+ const options = {
5066
+ command,
5067
+ cascade: false,
5068
+ final: false,
5069
+ yes: false,
5070
+ json: false,
5071
+ jsonl: false,
5072
+ continueSession: false,
5073
+ implicitCommand: true
5074
+ };
3998
5075
  let positionalOnly = false;
3999
5076
  for (let index = 0;index < argv.length; index += 1) {
4000
5077
  const argument = argv[index];
@@ -4028,6 +5105,21 @@ var parseCliArgs = (argv) => {
4028
5105
  options.model = optionValue(argv, index, argument);
4029
5106
  index += 1;
4030
5107
  break;
5108
+ case "--route": {
5109
+ const value = optionValue(argv, index, argument);
5110
+ try {
5111
+ parseHarnessModelRoute(value);
5112
+ } catch (error) {
5113
+ throw new CliUsageError(error instanceof Error ? error.message : String(error));
5114
+ }
5115
+ options.routes ??= [];
5116
+ options.routes.push(value);
5117
+ if (options.routes.length > HARNESS_SUBAGENT_PROFILES.length) {
5118
+ throw new CliUsageError(`--route cannot be repeated more than ${HARNESS_SUBAGENT_PROFILES.length} times.`);
5119
+ }
5120
+ index += 1;
5121
+ break;
5122
+ }
4031
5123
  case "--workspace":
4032
5124
  options.workspace = optionValue(argv, index, argument);
4033
5125
  index += 1;
@@ -4251,6 +5343,16 @@ var parseCliArgs = (argv) => {
4251
5343
  case "--json":
4252
5344
  options.json = true;
4253
5345
  break;
5346
+ case "--jsonl":
5347
+ options.jsonl = true;
5348
+ break;
5349
+ case "--session":
5350
+ options.sessionId = optionValue(argv, index, argument);
5351
+ index += 1;
5352
+ break;
5353
+ case "--continue":
5354
+ options.continueSession = true;
5355
+ break;
4254
5356
  case "--status": {
4255
5357
  const value = optionValue(argv, index, argument);
4256
5358
  if (!RUN_STATUSES.has(value)) {
@@ -4312,6 +5414,16 @@ var parseCliArgs = (argv) => {
4312
5414
  positional.push(argument);
4313
5415
  }
4314
5416
  }
5417
+ options.implicitCommand = !commandWasExplicit;
5418
+ if (options.json && options.jsonl) {
5419
+ throw new CliUsageError("You cannot combine --json and --jsonl.");
5420
+ }
5421
+ if (options.sessionId && options.continueSession) {
5422
+ throw new CliUsageError("You cannot combine --session and --continue.");
5423
+ }
5424
+ if (options.jsonl && options.command !== "run" && options.command !== "resume") {
5425
+ throw new CliUsageError("--jsonl is supported by run and resume.");
5426
+ }
4315
5427
  if (options.command === "resume") {
4316
5428
  const runId = positional.shift();
4317
5429
  if (runId) {
@@ -4339,6 +5451,28 @@ var parseCliArgs = (argv) => {
4339
5451
  if (positional.length > 0) {
4340
5452
  throw new CliUsageError(`runs ${runsCommand} received unexpected positional arguments.`);
4341
5453
  }
5454
+ } else if (options.command === "sessions") {
5455
+ const sessionsCommand = positional.shift();
5456
+ if (!sessionsCommand || !SESSIONS_COMMANDS.has(sessionsCommand)) {
5457
+ throw new CliUsageError("sessions requires one of: list, inspect, rename, fork, archive.");
5458
+ }
5459
+ options.sessionsCommand = sessionsCommand;
5460
+ if (sessionsCommand !== "list") {
5461
+ const sessionId2 = positional.shift();
5462
+ if (!sessionId2)
5463
+ throw new CliUsageError(`sessions ${sessionsCommand} requires a sessionId.`);
5464
+ options.sessionId = sessionId2;
5465
+ }
5466
+ if (sessionsCommand === "rename") {
5467
+ const title = positional.join(" ").trim();
5468
+ if (!title)
5469
+ throw new CliUsageError("sessions rename requires a title.");
5470
+ options.sessionTitle = title;
5471
+ positional.length = 0;
5472
+ }
5473
+ if (positional.length > 0) {
5474
+ throw new CliUsageError(`sessions ${sessionsCommand} received unexpected positional arguments.`);
5475
+ }
4342
5476
  } else if (options.command === "run" || options.command === "review") {
4343
5477
  const prompt = positional.join(" ").trim();
4344
5478
  if (prompt) {
@@ -4352,21 +5486,24 @@ var parseCliArgs = (argv) => {
4352
5486
  var help = `Zhivex Harness v${HARNESS_VERSION}
4353
5487
 
4354
5488
  Usage:
4355
- zhivex-harness run [options] "task"
4356
- zhivex-harness review [options] "review task"
4357
- zhivex-harness chat [options]
4358
- zhivex-harness providers [--json]
4359
- zhivex-harness doctor [options] [--json]
4360
- zhivex-harness resume [options] <runId> --approve|--deny
4361
- zhivex-harness runs list [--status <status>] [--limit <n>] [--json]
4362
- zhivex-harness runs inspect <runId> [--json]
4363
- zhivex-harness runs cancel <runId> [--reason <text>] [--cascade] [--final]
4364
- zhivex-harness runs cleanup --before <date|timestamp> [--status <status>]
4365
- zhivex-harness runs export <runId> [--json]
5489
+ zhx Start the interactive console
5490
+ zhx run [options] "task"
5491
+ zhx review [options] "review task"
5492
+ zhx chat [options] [--continue|--session <id>]
5493
+ zhx providers [--json]
5494
+ zhx doctor [options] [--json]
5495
+ zhx resume [options] <runId> --approve|--deny
5496
+ zhx runs list [--status <status>] [--limit <n>] [--json]
5497
+ zhx sessions list|inspect|rename|fork|archive
5498
+
5499
+ The long command zhivex-harness remains supported for compatibility.
4366
5500
 
4367
5501
  Options:
4368
- --provider <meta|qwen|openai> Provider (default: openai)
5502
+ --provider <${PROVIDERS.join("|")}> Provider (default: openai)
4369
5503
  --model <id> Override the default model
5504
+ --route <role=provider[:model]> Route a subagent role; repeatable
5505
+ --session <id> Open a durable interactive session
5506
+ --continue Open the latest durable interactive session
4370
5507
  --workspace <path> Target workspace (default: cwd)
4371
5508
  --state-dir <path> Durable run-state directory
4372
5509
  --mcp-config <path> Declarative governed MCP JSON configuration
@@ -4408,6 +5545,7 @@ Options:
4408
5545
  --max-parallel-reviews <1-4> Review group concurrency ceiling (default: 2)
4409
5546
  --yes Automatically approve writes and checks
4410
5547
  --json Emit structured final output
5548
+ --jsonl Stream redacted JSON Lines, then the final result
4411
5549
  -h, --help Show this help
4412
5550
  -v, --version Show the version
4413
5551
 
@@ -4420,7 +5558,8 @@ Exit codes:
4420
5558
  Credentials:
4421
5559
  OpenAI: OPENAI_API_KEY
4422
5560
  Meta: MODEL_API_KEY
4423
- Qwen: DASHSCOPE_API_KEY or QWEN_API_KEY`;
5561
+ Qwen: DASHSCOPE_API_KEY or QWEN_API_KEY
5562
+ Gemini: GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY`;
4424
5563
  var summarizeApproval = (approval) => {
4425
5564
  const mustShowCompleteProposal = approval.name === "apply_patch";
4426
5565
  const argumentsText = !mustShowCompleteProposal && approval.arguments.length > 1200 ? `${approval.arguments.slice(0, 1200)}\u2026` : approval.arguments;
@@ -4524,16 +5663,23 @@ var runResultDocument = (result, harness) => ({
4524
5663
  },
4525
5664
  stateDirectory: harness.config.stateDirectory
4526
5665
  });
4527
- var printTerminalResult = (result, harness, json, streamedText) => {
4528
- if (json) {
4529
- process.stdout.write(`${JSON.stringify(runResultDocument(result, harness), null, 2)}
5666
+ var printTerminalResult = (result, harness, output, tracker) => {
5667
+ const document = runResultDocument(result, harness);
5668
+ if (output.jsonl) {
5669
+ tracker.sequence = (tracker.sequence ?? 0) + 1;
5670
+ process.stdout.write(`${serializeStreamResult(document, tracker.sequence)}
4530
5671
  `);
4531
5672
  return;
4532
5673
  }
4533
- if (!streamedText && result.outputText) {
5674
+ if (output.json) {
5675
+ process.stdout.write(`${JSON.stringify(document, null, 2)}
5676
+ `);
5677
+ return;
5678
+ }
5679
+ if (!tracker.streamedText && result.outputText) {
4534
5680
  process.stdout.write(result.outputText);
4535
5681
  }
4536
- if (result.outputText || streamedText) {
5682
+ if (result.outputText || tracker.streamedText) {
4537
5683
  process.stdout.write(`
4538
5684
  `);
4539
5685
  }
@@ -4551,8 +5697,14 @@ ${summarizeApproval(approval)}
4551
5697
  `);
4552
5698
  }
4553
5699
  };
4554
- var streamSink = (json, tracker) => async (event) => {
4555
- if (!json && event.type === "text-delta") {
5700
+ var streamSink = (output, tracker) => async (event) => {
5701
+ if (output.jsonl) {
5702
+ tracker.sequence = (tracker.sequence ?? 0) + 1;
5703
+ process.stdout.write(`${serializeStreamEvent(event, tracker.sequence)}
5704
+ `);
5705
+ return;
5706
+ }
5707
+ if (!output.json && event.type === "text-delta") {
4556
5708
  tracker.streamedText = true;
4557
5709
  process.stdout.write(event.textDelta);
4558
5710
  }
@@ -4570,23 +5722,57 @@ subagent finish \xB7 ${event.childRun.agentId ?? event.childRun.toolName} \xB7 $
4570
5722
  `);
4571
5723
  }
4572
5724
  };
5725
+ var resolvedRouting = (options) => {
5726
+ return resolveHarnessModelRoutes(options.routes);
5727
+ };
5728
+ var assertRoutePricingIsSafe = (routes, costBudget) => {
5729
+ if (routes.size > 0 && costBudget !== undefined) {
5730
+ throw new CliUsageError("Cost budgets cannot be combined with model routes until pricing is configured per role.");
5731
+ }
5732
+ };
5733
+ var withTemporaryHarnessProfiles = async (profiles, configure, operation) => {
5734
+ await configure(profiles);
5735
+ try {
5736
+ return await operation();
5737
+ } finally {
5738
+ await configure([]);
5739
+ }
5740
+ };
5741
+ var createConfiguredHarness = async (options, extraProfiles = [], persistedRoutes) => {
5742
+ const routes = persistedRoutes ?? resolvedRouting(options);
5743
+ const resolvedConfig = resolveHarnessConfig(options);
5744
+ assertRoutePricingIsSafe(routes, resolvedConfig.costBudget);
5745
+ const profiles = [...new Set([
5746
+ ...options.subagentProfiles ?? [],
5747
+ ...extraProfiles,
5748
+ ...routes.keys()
5749
+ ])];
5750
+ const quietTelemetry = options.json || options.jsonl;
5751
+ const harness = await createHarness({
5752
+ ...options,
5753
+ subagentProfiles: profiles,
5754
+ subagentModels: createHarnessRouteModels(routes),
5755
+ onTelemetryEvent: orchestrationObserver(quietTelemetry)
5756
+ });
5757
+ return { harness, routes };
5758
+ };
4573
5759
  var runOnce = async (options) => {
4574
5760
  if (!options.prompt) {
4575
5761
  throw new CliUsageError('Missing task. Example: zhivex-harness run "fix the tests".');
4576
5762
  }
4577
- const harness = await createHarness({ ...options, onTelemetryEvent: orchestrationObserver(options.json) });
5763
+ const { harness, routes } = await createConfiguredHarness(options);
4578
5764
  try {
4579
5765
  const tracker = { streamedText: false };
4580
5766
  const result = await runHarness(harness, {
4581
5767
  prompt: options.prompt,
4582
5768
  scope: harness.config.scope,
4583
- metadata: createHarnessResumeMetadata(harness.config),
5769
+ metadata: createHarnessResumeMetadata(harness.config, routes),
4584
5770
  ...options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}
4585
5771
  }, {
4586
- onEvent: streamSink(options.json, tracker),
5772
+ onEvent: streamSink(options, tracker),
4587
5773
  resolveApprovals: terminalApprovalResolver(options.yes)
4588
5774
  });
4589
- printTerminalResult(result, harness, options.json, tracker.streamedText);
5775
+ printTerminalResult(result, harness, options, tracker);
4590
5776
  if (result.status === "failed" || result.status === "timed_out") {
4591
5777
  process.exitCode = CLI_EXIT_CODES.runtimeError;
4592
5778
  }
@@ -4603,11 +5789,7 @@ var reviewOnce = async (options) => {
4603
5789
  ...options.subagentProfiles ?? [],
4604
5790
  ...requestedReviewers
4605
5791
  ])];
4606
- const harness = await createHarness({
4607
- ...options,
4608
- subagentProfiles: enabledProfiles,
4609
- onTelemetryEvent: orchestrationObserver(options.json)
4610
- });
5792
+ const { harness } = await createConfiguredHarness(options, enabledProfiles);
4611
5793
  try {
4612
5794
  const result = await runHarnessReviewGroup(harness, { prompt: options.prompt, scope: harness.config.scope }, requestedReviewers);
4613
5795
  const document = {
@@ -4678,22 +5860,31 @@ ${summarizeApproval(approval)}
4678
5860
  `);
4679
5861
  }
4680
5862
  const persistedResumeInput = readHarnessResumeConfig(state);
5863
+ const persistedRoutes = readHarnessResumeRoutes(state);
4681
5864
  let persistedResumeOptions = {};
5865
+ let resumeCostBudget = initialConfig.costBudget;
4682
5866
  if (persistedResumeInput) {
4683
5867
  const persistedConfig = resolveHarnessConfig(persistedResumeInput);
4684
5868
  if (persistedConfig.workspace !== initialConfig.workspace || persistedConfig.stateDirectory !== initialConfig.stateDirectory || persistedConfig.storeBackend !== initialConfig.storeBackend || JSON.stringify(persistedConfig.scope) !== JSON.stringify(initialConfig.scope)) {
4685
5869
  throw new Error("Resume locator options do not match the persisted workspace, state directory, store, or scope.");
4686
5870
  }
4687
5871
  persistedResumeOptions = harnessConfigInput(persistedConfig);
5872
+ resumeCostBudget = persistedConfig.costBudget;
4688
5873
  }
5874
+ assertRoutePricingIsSafe(persistedRoutes, resumeCostBudget);
4689
5875
  const harness = await createHarness({
4690
5876
  ...persistedResumeOptions,
4691
5877
  ...options,
4692
5878
  provider: state.provider,
4693
5879
  model: state.modelId,
5880
+ subagentProfiles: [...new Set([
5881
+ ...persistedResumeOptions.subagentProfiles ?? [],
5882
+ ...persistedRoutes.keys()
5883
+ ])],
5884
+ subagentModels: createHarnessRouteModels(persistedRoutes),
4694
5885
  store: persistence.store,
4695
5886
  memory: persistence.memory,
4696
- onTelemetryEvent: orchestrationObserver(options.json)
5887
+ onTelemetryEvent: orchestrationObserver(options.json || options.jsonl)
4697
5888
  });
4698
5889
  const approve = options.approve;
4699
5890
  const tracker = { streamedText: false };
@@ -4701,10 +5892,11 @@ ${summarizeApproval(approval)}
4701
5892
  state,
4702
5893
  approvals: approvalResponses(state.pendingApprovals, approve, approve ? "Approved by resume --approve." : "Denied by resume --deny.")
4703
5894
  }, {
4704
- onEvent: streamSink(options.json, tracker),
5895
+ onEvent: streamSink(options, tracker),
4705
5896
  resolveApprovals: async (approvals) => approvalResponses(approvals, approve, approve ? "Approved by resume --approve." : "Denied by resume --deny.")
4706
5897
  });
4707
- printTerminalResult(result, harness, options.json, tracker.streamedText);
5898
+ await updateIndexedSessionRun(harness.config, result.state.runId, result.status);
5899
+ printTerminalResult(result, harness, options, tracker);
4708
5900
  if (result.status === "failed" || result.status === "timed_out") {
4709
5901
  process.exitCode = CLI_EXIT_CODES.runtimeError;
4710
5902
  }
@@ -4716,11 +5908,87 @@ var chat = async (options) => {
4716
5908
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
4717
5909
  throw new Error("Chat mode requires an interactive terminal. Use run for automation.");
4718
5910
  }
4719
- const harness = await createHarness({ ...options, onTelemetryEvent: orchestrationObserver(false) });
5911
+ const baseConfig = resolveHarnessConfig(options);
5912
+ const sessionStore = await openSessionStoreForConfig(baseConfig);
4720
5913
  const readline = createInterface({ input: process.stdin, output: process.stdout });
5914
+ let runtimeOptions = { ...options };
5915
+ let routes = resolvedRouting(options);
5916
+ const selectedSession = options.sessionId ? await sessionStore.get(options.sessionId) : options.continueSession ? await sessionStore.latest() : undefined;
5917
+ if (options.sessionId && !selectedSession) {
5918
+ sessionStore.close();
5919
+ readline.close();
5920
+ throw new Error(`Session ${options.sessionId} was not found.`);
5921
+ }
5922
+ let session = selectedSession ?? await sessionStore.create();
4721
5923
  let messages = [];
5924
+ const latestState = async (selected) => {
5925
+ const latest = selected.runs.at(-1);
5926
+ if (!latest)
5927
+ return;
5928
+ const persistence = await openHarnessPersistence(baseConfig);
5929
+ try {
5930
+ const state = await persistence.store.load(latest.runId, baseConfig.scope);
5931
+ if (!state)
5932
+ throw new Error(`Run ${latest.runId} referenced by session ${selected.sessionId} was not found.`);
5933
+ return state;
5934
+ } finally {
5935
+ persistence.close();
5936
+ }
5937
+ };
5938
+ let harness;
5939
+ try {
5940
+ const restored = await latestState(session);
5941
+ if (restored) {
5942
+ runtimeOptions = { ...runtimeOptions, provider: restored.provider, model: restored.modelId };
5943
+ routes = readHarnessResumeRoutes(restored);
5944
+ messages = restored.messages;
5945
+ }
5946
+ harness = (await createConfiguredHarness(runtimeOptions, [], routes)).harness;
5947
+ } catch (error) {
5948
+ readline.close();
5949
+ sessionStore.close();
5950
+ throw error;
5951
+ }
5952
+ const refreshSession = async () => {
5953
+ const refreshed = await sessionStore.get(session.sessionId);
5954
+ if (!refreshed)
5955
+ throw new Error(`Session ${session.sessionId} was not found.`);
5956
+ session = refreshed;
5957
+ return session;
5958
+ };
5959
+ const hasActiveTurn = async () => {
5960
+ const latest = (await refreshSession()).runs.at(-1);
5961
+ return latest && !["completed", "failed", "cancelled", "timed_out"].includes(latest.status) ? latest : undefined;
5962
+ };
5963
+ const replaceHarness = async (nextOptions, nextRoutes, extraProfiles = []) => {
5964
+ const created = await createConfiguredHarness(nextOptions, extraProfiles, nextRoutes);
5965
+ harness.close();
5966
+ harness = created.harness;
5967
+ runtimeOptions = nextOptions;
5968
+ routes = new Map(nextRoutes);
5969
+ };
5970
+ const withTemporaryProfiles = async (profiles, operation) => {
5971
+ const normalOptions = runtimeOptions;
5972
+ const normalRoutes = new Map(routes);
5973
+ return withTemporaryHarnessProfiles(profiles, (temporaryProfiles) => replaceHarness(normalOptions, normalRoutes, temporaryProfiles), operation);
5974
+ };
5975
+ const restoreSession = async (selected) => {
5976
+ const state = await latestState(selected);
5977
+ const nextOptions = state ? { ...runtimeOptions, provider: state.provider, model: state.modelId } : runtimeOptions;
5978
+ const nextRoutes = state ? readHarnessResumeRoutes(state) : resolveHarnessModelRoutes();
5979
+ await replaceHarness(nextOptions, nextRoutes);
5980
+ session = selected;
5981
+ messages = state?.messages ?? [];
5982
+ };
5983
+ const statusLine = async () => {
5984
+ const current = await refreshSession();
5985
+ const latest = current.runs.at(-1);
5986
+ const routeText = routes.size === 0 ? "default" : [...routes.values()].map((route) => `${route.profile}=${route.provider}:${route.model}`).join(", ");
5987
+ return `session ${current.sessionId} \xB7 ${harness.config.provider}/${harness.config.model}` + ` \xB7 ${messages.length} messages \xB7 routes ${routeText}` + `${latest ? ` \xB7 last ${latest.runId} (${latest.status})` : ""}`;
5988
+ };
4722
5989
  process.stderr.write(`Zhivex Harness ${HARNESS_VERSION} \xB7 ${harness.config.provider}/${harness.config.model}
4723
- ` + `Type /exit to quit or /clear to clear the context.
5990
+ ` + `session ${session.sessionId}
5991
+ ` + `Type /help for console commands.
4724
5992
  `);
4725
5993
  try {
4726
5994
  for (;; ) {
@@ -4732,39 +6000,253 @@ var chat = async (options) => {
4732
6000
  if (prompt === "/exit" || prompt === "/quit") {
4733
6001
  break;
4734
6002
  }
6003
+ if (prompt === "/help") {
6004
+ process.stderr.write(`/provider [id] \xB7 /model [id] \xB7 /route [role=provider[:model]] \xB7 /status
6005
+ ` + `/diff \xB7 /review <task> \xB7 /resume <last|sessionId> \xB7 /compact
6006
+ ` + `/new [title] \xB7 /rename <title> \xB7 /clear \xB7 /exit
6007
+ `);
6008
+ continue;
6009
+ }
4735
6010
  if (prompt === "/clear") {
4736
6011
  messages = [];
4737
6012
  process.stderr.write(`Context cleared.
4738
6013
  `);
4739
6014
  continue;
4740
6015
  }
4741
- const tracker = { streamedText: false };
4742
- const result = await runHarness(harness, messages.length === 0 ? {
4743
- prompt,
4744
- scope: harness.config.scope,
4745
- metadata: createHarnessResumeMetadata(harness.config)
4746
- } : {
4747
- messages: appendUserMessage(messages, prompt),
4748
- scope: harness.config.scope,
4749
- metadata: createHarnessResumeMetadata(harness.config)
4750
- }, {
4751
- onEvent: streamSink(false, tracker),
4752
- resolveApprovals: terminalApprovalResolver(options.yes, (question) => readline.question(question))
6016
+ if (prompt === "/status") {
6017
+ process.stderr.write(`${await statusLine()}
6018
+ `);
6019
+ continue;
6020
+ }
6021
+ if (prompt === "/diff") {
6022
+ const diff = await harness.workspace.gitDiff();
6023
+ const output = `${diff.status.stdout}${diff.diff.stdout}${diff.staged.stdout}`;
6024
+ process.stdout.write(output || `No workspace changes.
6025
+ `);
6026
+ continue;
6027
+ }
6028
+ if (prompt === "/compact") {
6029
+ messages = compactHarnessMessages(messages);
6030
+ process.stderr.write(`Context compacted to ${messages.length} redacted message(s).
6031
+ `);
6032
+ continue;
6033
+ }
6034
+ if (prompt === "/provider" || prompt.startsWith("/provider ")) {
6035
+ const value = prompt.slice("/provider".length).trim();
6036
+ if (!value) {
6037
+ process.stderr.write(`${providerAvailability().map((provider2) => `${provider2.id}${provider2.id === harness.config.provider ? "*" : ""}`).join(" ")}
6038
+ `);
6039
+ continue;
6040
+ }
6041
+ const active2 = await hasActiveTurn();
6042
+ if (active2) {
6043
+ process.stderr.write(`Cannot switch provider while run ${active2.runId} is ${active2.status}.
6044
+ `);
6045
+ continue;
6046
+ }
6047
+ const provider = parseProvider(value);
6048
+ const model = DEFAULT_PROVIDER_REGISTRY.descriptor(provider).defaultModel;
6049
+ const portableMessages = compactHarnessMessages(messages);
6050
+ await replaceHarness({ ...runtimeOptions, provider, model }, routes);
6051
+ messages = portableMessages;
6052
+ process.stderr.write(`Next turn: ${provider}/${model}; context was compacted for a safe handoff.
6053
+ `);
6054
+ continue;
6055
+ }
6056
+ if (prompt === "/model" || prompt.startsWith("/model ")) {
6057
+ const value = prompt.slice("/model".length).trim();
6058
+ if (!value) {
6059
+ process.stderr.write(`${harness.config.provider}/${harness.config.model}
6060
+ `);
6061
+ continue;
6062
+ }
6063
+ const active2 = await hasActiveTurn();
6064
+ if (active2) {
6065
+ process.stderr.write(`Cannot switch model while run ${active2.runId} is ${active2.status}.
6066
+ `);
6067
+ continue;
6068
+ }
6069
+ const portableMessages = compactHarnessMessages(messages);
6070
+ await replaceHarness({ ...runtimeOptions, model: value }, routes);
6071
+ messages = portableMessages;
6072
+ process.stderr.write(`Next turn: ${harness.config.provider}/${value}; context was compacted.
6073
+ `);
6074
+ continue;
6075
+ }
6076
+ if (prompt === "/route" || prompt.startsWith("/route ")) {
6077
+ const value = prompt.slice("/route".length).trim();
6078
+ if (!value) {
6079
+ process.stderr.write(`${JSON.stringify(serializeHarnessModelRoutes(routes))}
6080
+ `);
6081
+ continue;
6082
+ }
6083
+ const active2 = await hasActiveTurn();
6084
+ if (active2) {
6085
+ process.stderr.write(`Cannot change routes while run ${active2.runId} is ${active2.status}.
6086
+ `);
6087
+ continue;
6088
+ }
6089
+ const nextRoutes = new Map(routes);
6090
+ if (value === "clear") {
6091
+ nextRoutes.clear();
6092
+ } else if (value.startsWith("clear ")) {
6093
+ const profile = value.slice("clear ".length).trim();
6094
+ if (!HARNESS_SUBAGENT_PROFILES.includes(profile)) {
6095
+ process.stderr.write(`Unknown subagent profile: ${profile}.
6096
+ `);
6097
+ continue;
6098
+ }
6099
+ nextRoutes.delete(profile);
6100
+ } else {
6101
+ const route = parseHarnessModelRoute(value);
6102
+ nextRoutes.set(route.profile, route);
6103
+ }
6104
+ await replaceHarness(runtimeOptions, nextRoutes);
6105
+ process.stderr.write(`Routes: ${JSON.stringify(serializeHarnessModelRoutes(routes))}
6106
+ `);
6107
+ continue;
6108
+ }
6109
+ if (prompt === "/new" || prompt.startsWith("/new ")) {
6110
+ const active2 = await hasActiveTurn();
6111
+ if (active2) {
6112
+ process.stderr.write(`Cannot leave session while run ${active2.runId} is ${active2.status}.
6113
+ `);
6114
+ continue;
6115
+ }
6116
+ const title = prompt.slice("/new".length).trim();
6117
+ const created = await sessionStore.create(title ? { title } : undefined);
6118
+ await restoreSession(created);
6119
+ process.stderr.write(`Created session ${session.sessionId}.
6120
+ `);
6121
+ continue;
6122
+ }
6123
+ if (prompt === "/rename" || prompt.startsWith("/rename ")) {
6124
+ const title = prompt.slice("/rename".length).trim();
6125
+ if (!title) {
6126
+ process.stderr.write(`Usage: /rename <title>
6127
+ `);
6128
+ continue;
6129
+ }
6130
+ session = await sessionStore.rename(session.sessionId, title);
6131
+ process.stderr.write(`Renamed session to ${session.title}.
6132
+ `);
6133
+ continue;
6134
+ }
6135
+ if (prompt === "/resume" || prompt.startsWith("/resume ")) {
6136
+ const selector = prompt.slice("/resume".length).trim() || "last";
6137
+ const selected = selector === "last" ? await sessionStore.latest({ includeArchived: true }) : await sessionStore.get(selector);
6138
+ if (!selected) {
6139
+ process.stderr.write(`Session ${selector} was not found.
6140
+ `);
6141
+ continue;
6142
+ }
6143
+ await restoreSession(selected);
6144
+ const active2 = await hasActiveTurn();
6145
+ process.stderr.write(active2 ? `Session ${session.sessionId} has run ${active2.runId} waiting in ${active2.status}; use zhx resume ${active2.runId} --approve|--deny.
6146
+ ` : `Resumed session ${session.sessionId}.
6147
+ `);
6148
+ continue;
6149
+ }
6150
+ if (prompt === "/review" || prompt.startsWith("/review ")) {
6151
+ const reviewPrompt = prompt.slice("/review".length).trim();
6152
+ if (!reviewPrompt) {
6153
+ process.stderr.write(`Usage: /review <task>
6154
+ `);
6155
+ continue;
6156
+ }
6157
+ const active2 = await hasActiveTurn();
6158
+ if (active2) {
6159
+ process.stderr.write(`Cannot start a review while run ${active2.runId} is ${active2.status}.
6160
+ `);
6161
+ continue;
6162
+ }
6163
+ const review = await withTemporaryProfiles(["explorer", "reviewer"], () => runHarnessReviewGroup(harness, { prompt: reviewPrompt, scope: harness.config.scope }, ["explorer", "reviewer"]));
6164
+ for (const output of review.outputs) {
6165
+ process.stdout.write(`
6166
+ [${output.name ?? output.agentId ?? "reviewer"}] ${output.status}
6167
+ `);
6168
+ if (output.output?.outputText)
6169
+ process.stdout.write(`${output.output.outputText}
6170
+ `);
6171
+ }
6172
+ continue;
6173
+ }
6174
+ const active = await hasActiveTurn();
6175
+ if (active) {
6176
+ process.stderr.write(`Run ${active.runId} is ${active.status}; resolve it with zhx resume ${active.runId} --approve|--deny before a new turn.
6177
+ `);
6178
+ continue;
6179
+ }
6180
+ const runId = `run_${randomUUID5()}`;
6181
+ session = await sessionStore.appendRun(session.sessionId, {
6182
+ runId,
6183
+ provider: harness.config.provider,
6184
+ model: harness.config.model,
6185
+ status: "created"
4753
6186
  });
6187
+ const turn = session.runs.at(-1);
6188
+ const tracker = { streamedText: false };
6189
+ let markedRunning = false;
6190
+ let result;
6191
+ try {
6192
+ result = await runHarness(harness, messages.length === 0 ? {
6193
+ runId,
6194
+ prompt,
6195
+ scope: harness.config.scope,
6196
+ metadata: {
6197
+ ...createHarnessResumeMetadata(harness.config, routes),
6198
+ zhivexCliSession: {
6199
+ schemaVersion: 1,
6200
+ sessionId: session.sessionId,
6201
+ turnId: turn.turnId,
6202
+ sequence: turn.sequence
6203
+ }
6204
+ }
6205
+ } : {
6206
+ runId,
6207
+ messages: appendUserMessage(messages, prompt),
6208
+ scope: harness.config.scope,
6209
+ metadata: {
6210
+ ...createHarnessResumeMetadata(harness.config, routes),
6211
+ zhivexCliSession: {
6212
+ schemaVersion: 1,
6213
+ sessionId: session.sessionId,
6214
+ turnId: turn.turnId,
6215
+ sequence: turn.sequence
6216
+ }
6217
+ }
6218
+ }, {
6219
+ onEvent: async (event) => {
6220
+ if (!markedRunning && event.type === "agent-run-start") {
6221
+ session = await sessionStore.updateRun(session.sessionId, runId, { status: "running" });
6222
+ markedRunning = true;
6223
+ }
6224
+ await streamSink({ json: false, jsonl: false }, tracker)(event);
6225
+ },
6226
+ resolveApprovals: terminalApprovalResolver(options.yes, (question) => readline.question(question))
6227
+ });
6228
+ } catch (error) {
6229
+ session = await sessionStore.updateRun(session.sessionId, runId, { status: "failed" });
6230
+ throw error;
6231
+ }
4754
6232
  if (!tracker.streamedText && result.outputText) {
4755
6233
  process.stdout.write(result.outputText);
4756
6234
  }
4757
6235
  process.stdout.write(`
4758
6236
  `);
4759
6237
  messages = result.messages;
6238
+ session = await sessionStore.updateRun(session.sessionId, runId, {
6239
+ status: sessionStatus(result.status)
6240
+ });
4760
6241
  if (result.status === "waiting_approval") {
4761
- process.stderr.write(`Run paused: ${result.state.runId}
6242
+ process.stderr.write(`Run paused: ${result.state.runId}. Resume with zhx resume ${result.state.runId} --approve|--deny.
4762
6243
  `);
4763
6244
  }
4764
6245
  }
4765
6246
  } finally {
4766
6247
  readline.close();
4767
6248
  harness.close();
6249
+ sessionStore.close();
4768
6250
  }
4769
6251
  };
4770
6252
  var providersDocument = (env = process.env) => ({
@@ -4819,7 +6301,7 @@ var satisfiesBunEngine = (version, range) => {
4819
6301
  var diagnostic = (id, status, message, details = {}) => ({ id, status, message, details });
4820
6302
  var inspectWorkspace = async (workspace) => {
4821
6303
  try {
4822
- const workspaceStat = await stat3(workspace);
6304
+ const workspaceStat = await stat4(workspace);
4823
6305
  if (!workspaceStat.isDirectory()) {
4824
6306
  return diagnostic("workspace", "fail", "Workspace is not a directory.", { path: workspace });
4825
6307
  }
@@ -4861,9 +6343,9 @@ var inspectGit = async (workspace) => {
4861
6343
  }
4862
6344
  };
4863
6345
  var inspectScripts = async (workspace, allowedChecks) => {
4864
- const packagePath = path8.join(workspace, "package.json");
6346
+ const packagePath = path9.join(workspace, "package.json");
4865
6347
  try {
4866
- const packageStat = await stat3(packagePath);
6348
+ const packageStat = await stat4(packagePath);
4867
6349
  if (!packageStat.isFile() || packageStat.size > 1024 * 1024) {
4868
6350
  return diagnostic("scripts", "warn", "package.json is not a readable regular file.", {
4869
6351
  packageJson: false,
@@ -4885,17 +6367,17 @@ var inspectScripts = async (workspace, allowedChecks) => {
4885
6367
  }
4886
6368
  };
4887
6369
  var isInsidePath2 = (root, candidate) => {
4888
- const relative = path8.relative(root, candidate);
4889
- return relative === "" || !relative.startsWith(`..${path8.sep}`) && relative !== ".." && !path8.isAbsolute(relative);
6370
+ const relative = path9.relative(root, candidate);
6371
+ return relative === "" || !relative.startsWith(`..${path9.sep}`) && relative !== ".." && !path9.isAbsolute(relative);
4890
6372
  };
4891
6373
  var containsSymlink = async (candidate, trustedPrefix) => {
4892
- const parsed = path8.parse(candidate);
4893
- const parts = trustedPrefix ? path8.relative(trustedPrefix, candidate).split(path8.sep).filter(Boolean) : candidate.slice(parsed.root.length).split(path8.sep).filter(Boolean).slice(1);
4894
- let current = trustedPrefix ?? path8.join(parsed.root, candidate.slice(parsed.root.length).split(path8.sep).filter(Boolean)[0] ?? "");
6374
+ const parsed = path9.parse(candidate);
6375
+ const parts = trustedPrefix ? path9.relative(trustedPrefix, candidate).split(path9.sep).filter(Boolean) : candidate.slice(parsed.root.length).split(path9.sep).filter(Boolean).slice(1);
6376
+ let current = trustedPrefix ?? path9.join(parsed.root, candidate.slice(parsed.root.length).split(path9.sep).filter(Boolean)[0] ?? "");
4895
6377
  for (const part of parts) {
4896
- current = path8.join(current, part);
6378
+ current = path9.join(current, part);
4897
6379
  try {
4898
- if ((await lstat6(current)).isSymbolicLink()) {
6380
+ if ((await lstat7(current)).isSymbolicLink()) {
4899
6381
  return true;
4900
6382
  }
4901
6383
  } catch (error) {
@@ -4911,12 +6393,12 @@ var nearestExistingPath = async (candidate) => {
4911
6393
  let current = candidate;
4912
6394
  for (;; ) {
4913
6395
  try {
4914
- return { path: current, stat: await stat3(current) };
6396
+ return { path: current, stat: await stat4(current) };
4915
6397
  } catch (error) {
4916
6398
  if (error.code !== "ENOENT") {
4917
6399
  throw error;
4918
6400
  }
4919
- const parent = path8.dirname(current);
6401
+ const parent = path9.dirname(current);
4920
6402
  if (parent === current) {
4921
6403
  throw error;
4922
6404
  }
@@ -4932,7 +6414,7 @@ var inspectStateDirectory = async (workspace, stateDirectory) => {
4932
6414
  });
4933
6415
  }
4934
6416
  if (isInsidePath2(workspace, stateDirectory)) {
4935
- const segments = path8.relative(workspace, stateDirectory).split(path8.sep).filter(Boolean);
6417
+ const segments = path9.relative(workspace, stateDirectory).split(path9.sep).filter(Boolean);
4936
6418
  const sensitiveSegment = segments.find(isSensitiveStateSegment);
4937
6419
  if (sensitiveSegment) {
4938
6420
  return diagnostic("state-directory", "fail", "State directory is inside a sensitive workspace path.", {
@@ -4979,9 +6461,9 @@ var inspectOperationsStore = async (stateDirectory, storeBackend) => {
4979
6461
  migrationAvailable: true
4980
6462
  });
4981
6463
  }
4982
- const databasePath = path8.join(stateDirectory, "operations.sqlite");
6464
+ const databasePath = path9.join(stateDirectory, "operations.sqlite");
4983
6465
  try {
4984
- const entry = await lstat6(databasePath);
6466
+ const entry = await lstat7(databasePath);
4985
6467
  if (entry.isSymbolicLink() || !entry.isFile()) {
4986
6468
  return diagnostic("operations-store", "fail", "SQLite state path is not a regular file.", {
4987
6469
  backend: storeBackend,
@@ -5269,6 +6751,95 @@ var manageRuns = async (options) => {
5269
6751
  persistence.close();
5270
6752
  }
5271
6753
  };
6754
+ var openSessionStoreForConfig = (config) => openCliSessionStore({
6755
+ workspace: config.workspace,
6756
+ stateDirectory: config.stateDirectory,
6757
+ scope: config.scope
6758
+ });
6759
+ var printSessionDocument = (document, json) => {
6760
+ if (json) {
6761
+ process.stdout.write(`${JSON.stringify(document, null, 2)}
6762
+ `);
6763
+ return;
6764
+ }
6765
+ const record = document;
6766
+ if (record.kind === "session-list") {
6767
+ for (const session2 of record.sessions ?? []) {
6768
+ const latest = session2.runs.at(-1);
6769
+ process.stdout.write(`${session2.sessionId} ${session2.title ?? "(untitled)"} \xB7 ${session2.runs.length} runs` + `${latest ? ` \xB7 ${latest.provider}/${latest.model} \xB7 ${latest.status}` : ""}
6770
+ `);
6771
+ }
6772
+ return;
6773
+ }
6774
+ const session = record.session;
6775
+ if (session) {
6776
+ process.stdout.write(`${session.sessionId} \xB7 ${session.title ?? "(untitled)"} \xB7 ${session.runs.length} runs` + `${session.archivedAt ? " \xB7 archived" : ""}
6777
+ `);
6778
+ return;
6779
+ }
6780
+ process.stdout.write(`${JSON.stringify(document, null, 2)}
6781
+ `);
6782
+ };
6783
+ var manageSessions = async (options) => {
6784
+ const config = resolveHarnessConfig(options);
6785
+ const store = await openSessionStoreForConfig(config);
6786
+ try {
6787
+ let session;
6788
+ let document;
6789
+ switch (options.sessionsCommand) {
6790
+ case "list": {
6791
+ const summaries = await store.list(options.limit ? { limit: options.limit } : undefined);
6792
+ const sessions = (await Promise.all(summaries.map((summary) => store.get(summary.sessionId)))).filter((value) => Boolean(value));
6793
+ document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session-list", sessions };
6794
+ break;
6795
+ }
6796
+ case "inspect":
6797
+ session = await store.get(options.sessionId);
6798
+ if (!session)
6799
+ throw new Error(`Session ${options.sessionId} was not found.`);
6800
+ document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
6801
+ break;
6802
+ case "rename":
6803
+ session = await store.rename(options.sessionId, options.sessionTitle);
6804
+ document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
6805
+ break;
6806
+ case "fork":
6807
+ session = await store.fork(options.sessionId);
6808
+ document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
6809
+ break;
6810
+ case "archive":
6811
+ session = await store.archive(options.sessionId);
6812
+ document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
6813
+ break;
6814
+ default:
6815
+ throw new CliUsageError("sessions requires one of: list, inspect, rename, fork, archive.");
6816
+ }
6817
+ printSessionDocument(document, options.json);
6818
+ } finally {
6819
+ store.close();
6820
+ }
6821
+ };
6822
+ var sessionStatus = (status) => {
6823
+ if (status === "queued")
6824
+ return "created";
6825
+ if (status === "suspended")
6826
+ return "waiting_approval";
6827
+ return status;
6828
+ };
6829
+ var updateIndexedSessionRun = async (config, runId, status) => {
6830
+ const store = await openSessionStoreForConfig(config);
6831
+ try {
6832
+ for (const summary of await store.list({ limit: 200, includeArchived: true })) {
6833
+ const session = await store.get(summary.sessionId);
6834
+ if (session?.runs.some((run) => run.runId === runId)) {
6835
+ await store.updateRun(session.sessionId, runId, { status: sessionStatus(status) });
6836
+ return;
6837
+ }
6838
+ }
6839
+ } finally {
6840
+ store.close();
6841
+ }
6842
+ };
5272
6843
  var cliExitCodeForError = (error) => error instanceof CliUsageError ? CLI_EXIT_CODES.usageError : CLI_EXIT_CODES.runtimeError;
5273
6844
  var main = async (argv = process.argv.slice(2)) => {
5274
6845
  const options = parseCliArgs(argv);
@@ -5296,7 +6867,14 @@ var main = async (argv = process.argv.slice(2)) => {
5296
6867
  case "runs":
5297
6868
  await manageRuns(options);
5298
6869
  return;
6870
+ case "sessions":
6871
+ await manageSessions(options);
6872
+ return;
5299
6873
  case "run":
6874
+ if (options.implicitCommand && !options.prompt && process.stdin.isTTY && process.stdout.isTTY) {
6875
+ await chat(options);
6876
+ return;
6877
+ }
5300
6878
  await runOnce(options);
5301
6879
  return;
5302
6880
  case "review":
@@ -5312,9 +6890,11 @@ if (import.meta.main) {
5312
6890
  });
5313
6891
  }
5314
6892
  export {
6893
+ withTemporaryHarnessProfiles,
5315
6894
  summarizeApproval,
5316
6895
  runResultDocument,
5317
6896
  resumeCommand,
6897
+ readHarnessResumeRoutes,
5318
6898
  readHarnessResumeConfig,
5319
6899
  providersDocument,
5320
6900
  parseCliArgs,