@zhivex-ai/harness 0.6.1 → 0.7.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.4",
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)
@@ -3591,32 +3795,49 @@ var createExecutionEnvironmentTools = (workspace) => ({
3591
3795
  })
3592
3796
  });
3593
3797
  var estimateMessageTokens = (messages) => Math.max(1, Math.ceil(JSON.stringify(messages).length / 4));
3594
- var createHarnessCompactor = () => {
3798
+ var summarizeHarnessMessages = (messages) => {
3595
3799
  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(" | ")}`;
3800
+ const summaries = messages.map((message, index) => {
3801
+ const record = message;
3802
+ const parts = (record.parts ?? []).map((part) => {
3803
+ if (part.type === "text" && typeof part.text === "string") {
3804
+ 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();
3805
+ return text.length > 160 ? `${text.slice(0, 160)}\u2026` : text;
3806
+ }
3807
+ if (part.type === "tool-call") {
3808
+ const call = part.toolCall;
3809
+ return `tool-call:${typeof call?.name === "string" ? call.name : "unknown"}`;
3810
+ }
3811
+ if (part.type === "tool-result") {
3812
+ const result = part.toolResult;
3813
+ return `tool-result:${typeof result?.toolName === "string" ? result.toolName : "unknown"}`;
3814
+ }
3815
+ return typeof part.type === "string" ? part.type : "part";
3615
3816
  });
3616
- const summary = summaries.join(`
3817
+ return `${index + 1}. ${record.role ?? "message"}: ${parts.join(" | ")}`;
3818
+ });
3819
+ const summary = summaries.join(`
3617
3820
  `);
3821
+ return summary.length > 4000 ? `${summary.slice(0, 4000)}\u2026` : summary;
3822
+ };
3823
+ var compactHarnessMessages = (messages) => {
3824
+ if (messages.length === 0)
3825
+ return [];
3826
+ return [
3827
+ {
3828
+ role: "user",
3829
+ parts: [{
3830
+ type: "text",
3831
+ text: `[Compacted conversation context]
3832
+ ${summarizeHarnessMessages(messages)}`
3833
+ }]
3834
+ }
3835
+ ];
3836
+ };
3837
+ var createHarnessCompactor = () => {
3838
+ return async ({ messages }) => {
3618
3839
  return {
3619
- summary: summary.length > 4000 ? `${summary.slice(0, 4000)}\u2026` : summary,
3840
+ summary: summarizeHarnessMessages(messages),
3620
3841
  metadata: {
3621
3842
  strategy: "deterministic-redacted-transcript",
3622
3843
  sourceMessages: messages.length
@@ -3679,7 +3900,7 @@ var createProviderCompatibleBudget = (config) => {
3679
3900
  };
3680
3901
  };
3681
3902
  var createHarness = async (options = {}) => {
3682
- const config = resolveHarnessConfig(options);
3903
+ const config = resolveHarnessConfig(options, options.providerRegistry);
3683
3904
  const workspace = await Workspace.open(config.workspace);
3684
3905
  await validateStateDirectory(config.workspace, config.stateDirectory);
3685
3906
  const executionEnvironment = config.execution.backend === "oci" ? await createHarnessOciExecutionEnvironment({
@@ -3688,7 +3909,7 @@ var createHarness = async (options = {}) => {
3688
3909
  stateDirectory: config.stateDirectory,
3689
3910
  ...options.ociRuntimeAdapter ? { runtime: options.ociRuntimeAdapter } : {}
3690
3911
  }) : undefined;
3691
- const model = options.modelInstance ?? createProviderModel(config, options.env ?? process.env);
3912
+ const model = options.modelInstance ?? createProviderModel(config, options.env ?? process.env, options.providerRegistry);
3692
3913
  const capabilityRequirements = [...new Set([
3693
3914
  ...config.requiredCapabilities,
3694
3915
  ...config.orchestration.profiles.length > 0 || config.mcpConfigPath || options.mcpConfiguration ? ["tools", "streaming"] : []
@@ -3730,7 +3951,7 @@ var createHarness = async (options = {}) => {
3730
3951
  await options.onTelemetryEvent?.(event);
3731
3952
  };
3732
3953
  const costGuardrails = createCostGuardrails(config);
3733
- const binding = createHarnessBinding(config, mcpConfiguration, model, options.subagentModels, executionEnvironment);
3954
+ const binding = createHarnessBinding(config, mcpConfiguration, model, options.subagentModels, (options.providerRegistry ?? DEFAULT_PROVIDER_REGISTRY).transportFingerprint(options.env ?? process.env), executionEnvironment);
3734
3955
  const subagentRuntime = createHarnessSubagents({
3735
3956
  config,
3736
3957
  parentBinding: binding,
@@ -3862,33 +4083,851 @@ var appendUserMessage = (messages, text) => [
3862
4083
  import {
3863
4084
  estimateAgentRunCost
3864
4085
  } 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
4086
 
3888
- class CliUsageError extends Error {
3889
- constructor(message) {
3890
- super(message);
3891
- this.name = "CliUsageError";
4087
+ // src/routing.ts
4088
+ var routeError = (value) => new Error(`Invalid route: ${value}. Use <profile>=<provider>[:<model>].`);
4089
+ var parseHarnessModelRoute = (value, registry = DEFAULT_PROVIDER_REGISTRY) => {
4090
+ const separator = value.indexOf("=");
4091
+ if (separator <= 0 || separator === value.length - 1) {
4092
+ throw routeError(value);
4093
+ }
4094
+ const profile = value.slice(0, separator).trim().toLowerCase();
4095
+ if (!HARNESS_SUBAGENT_PROFILES.includes(profile)) {
4096
+ throw routeError(value);
4097
+ }
4098
+ const target = value.slice(separator + 1).trim();
4099
+ const modelSeparator = target.indexOf(":");
4100
+ const providerInput = (modelSeparator === -1 ? target : target.slice(0, modelSeparator)).trim();
4101
+ const provider = registry.parse(providerInput);
4102
+ const configuredModel = modelSeparator === -1 ? "" : target.slice(modelSeparator + 1).trim();
4103
+ if (modelSeparator !== -1 && !configuredModel) {
4104
+ throw routeError(value);
4105
+ }
4106
+ const model = configuredModel || registry.descriptor(provider).defaultModel;
4107
+ if (/[\u0000-\u001f\u007f]/.test(model) || model.length > 256) {
4108
+ throw routeError(value);
4109
+ }
4110
+ return Object.freeze({
4111
+ profile,
4112
+ provider,
4113
+ model
4114
+ });
4115
+ };
4116
+ var resolveHarnessModelRoutes = (values = [], registry = DEFAULT_PROVIDER_REGISTRY) => {
4117
+ const routes = new Map;
4118
+ for (const value of values) {
4119
+ const route = parseHarnessModelRoute(value, registry);
4120
+ if (routes.has(route.profile)) {
4121
+ throw new Error(`Duplicate model route for ${route.profile}.`);
4122
+ }
4123
+ routes.set(route.profile, route);
4124
+ }
4125
+ return routes;
4126
+ };
4127
+ var createHarnessRouteModels = (routes, env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => Object.fromEntries([...routes].map(([profile, route]) => [
4128
+ profile,
4129
+ registry.createModel(route, env)
4130
+ ]));
4131
+ var serializeHarnessModelRoutes = (routes) => Object.fromEntries([...routes].map(([profile, route]) => [
4132
+ profile,
4133
+ { provider: route.provider, model: route.model }
4134
+ ]));
4135
+
4136
+ // src/cli-stream.ts
4137
+ var CLI_JSON_SCHEMA_VERSION = 1;
4138
+ var CLI_EVENT_SCHEMA_VERSION = 1;
4139
+ var streamEventDocument = (event, sequence = 0) => {
4140
+ if (!Number.isSafeInteger(sequence) || sequence < 0) {
4141
+ throw new Error("JSONL event sequence must be a non-negative integer.");
4142
+ }
4143
+ const base = {
4144
+ schemaVersion: CLI_EVENT_SCHEMA_VERSION,
4145
+ kind: "run-event",
4146
+ sequence,
4147
+ type: event.type
4148
+ };
4149
+ switch (event.type) {
4150
+ case "text-delta":
4151
+ return { ...base, textDelta: event.textDelta };
4152
+ case "tool-call":
4153
+ return { ...base, toolCallId: event.toolCall.id, toolName: event.toolCall.name };
4154
+ case "tool-result":
4155
+ return {
4156
+ ...base,
4157
+ toolCallId: event.toolResult.toolCallId,
4158
+ toolName: event.toolResult.toolName,
4159
+ isError: event.toolResult.isError
4160
+ };
4161
+ case "tool-approval-request":
4162
+ case "agent-approval-request":
4163
+ return {
4164
+ ...base,
4165
+ approvalRequestId: event.approval.id,
4166
+ approvalKind: event.approval.kind ?? "provider",
4167
+ toolName: event.approval.name
4168
+ };
4169
+ case "agent-approval-resolved":
4170
+ return {
4171
+ ...base,
4172
+ approvalRequestId: event.approval.approvalRequestId,
4173
+ approved: event.approval.approve
4174
+ };
4175
+ case "provider-data":
4176
+ return { ...base, provider: event.provider };
4177
+ case "image-generation":
4178
+ return {
4179
+ ...base,
4180
+ provider: event.provider,
4181
+ partial: event.partial,
4182
+ ...event.id ? { id: event.id } : {},
4183
+ ...event.index !== undefined ? { index: event.index } : {}
4184
+ };
4185
+ case "finish":
4186
+ return {
4187
+ ...base,
4188
+ ...event.finishReason ? { finishReason: event.finishReason } : {},
4189
+ ...event.usage ? { usage: event.usage } : {}
4190
+ };
4191
+ case "error":
4192
+ return { ...base, error: "Provider stream failed." };
4193
+ case "agent-run-start":
4194
+ return { ...base, currentStep: event.currentStep, maxSteps: event.maxSteps };
4195
+ case "agent-step-start":
4196
+ return { ...base, stepIndex: event.stepIndex };
4197
+ case "agent-step-finish":
4198
+ return {
4199
+ ...base,
4200
+ stepIndex: event.step.index,
4201
+ status: event.step.status,
4202
+ toolCalls: event.step.toolResults.length
4203
+ };
4204
+ case "agent-compaction":
4205
+ return {
4206
+ ...base,
4207
+ compactionId: event.compaction.id,
4208
+ reasons: event.compaction.reasons,
4209
+ messageCountBefore: event.compaction.messageCountBefore,
4210
+ messageCountAfter: event.compaction.messageCountAfter
4211
+ };
4212
+ case "agent-run-finish":
4213
+ return {
4214
+ ...base,
4215
+ runId: event.state.runId,
4216
+ status: event.status,
4217
+ provider: event.state.provider,
4218
+ model: event.state.modelId
4219
+ };
4220
+ }
4221
+ };
4222
+ var serializeStreamEvent = (event, sequence = 0) => JSON.stringify(streamEventDocument(event, sequence));
4223
+ var streamResultDocument = (result, sequence = 0) => {
4224
+ if (!Number.isSafeInteger(sequence) || sequence < 0) {
4225
+ throw new Error("JSONL result sequence must be a non-negative integer.");
4226
+ }
4227
+ return {
4228
+ schemaVersion: CLI_EVENT_SCHEMA_VERSION,
4229
+ kind: "run-result",
4230
+ sequence,
4231
+ runId: result.runId,
4232
+ status: result.status,
4233
+ provider: result.provider,
4234
+ model: result.model,
4235
+ steps: result.steps,
4236
+ toolCalls: result.toolCalls,
4237
+ pendingApprovals: (result.pendingApprovals ?? []).map((approval) => ({
4238
+ id: approval.id,
4239
+ kind: approval.kind,
4240
+ name: approval.name,
4241
+ ...approval.childRunId ? { childRunId: approval.childRunId } : {},
4242
+ ...approval.childAgentId ? { childAgentId: approval.childAgentId } : {}
4243
+ })),
4244
+ children: (result.children ?? []).map((child) => ({
4245
+ runId: child.runId,
4246
+ status: child.status
4247
+ }))
4248
+ };
4249
+ };
4250
+ var serializeStreamResult = (result, sequence = 0) => JSON.stringify(streamResultDocument(result, sequence));
4251
+
4252
+ // src/sessions.ts
4253
+ import { createHash as createHash8, randomUUID as randomUUID4 } from "crypto";
4254
+ import { chmod as chmod3, lstat as lstat6, mkdir as mkdir4, open as open3, realpath as realpath4, stat as stat3 } from "fs/promises";
4255
+ import path8 from "path";
4256
+ import { createRedactionPolicy as createRedactionPolicy3 } from "@zhivex-ai/agents";
4257
+ import { Database as Database2 } from "bun:sqlite";
4258
+ var HARNESS_SESSION_INDEX_FILE = "operations.sqlite";
4259
+ var HARNESS_SESSION_SCHEMA_VERSION = 1;
4260
+ var SESSION_RUN_STATUSES = [
4261
+ "created",
4262
+ "running",
4263
+ "waiting_approval",
4264
+ "cancel_requested",
4265
+ "completed",
4266
+ "failed",
4267
+ "cancelled",
4268
+ "timed_out"
4269
+ ];
4270
+ var TERMINAL_SESSION_RUN_STATUSES = [
4271
+ "completed",
4272
+ "failed",
4273
+ "cancelled",
4274
+ "timed_out"
4275
+ ];
4276
+ var DEFAULT_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
4277
+ var DEFAULT_MAX_SESSIONS = 200;
4278
+ var DEFAULT_MAX_RUNS = 1000;
4279
+ var DEFAULT_MAX_METADATA_BYTES = 4096;
4280
+ var DEFAULT_MAX_INDEX_BYTES = 16 * 1024 * 1024;
4281
+ var MAX_LIST_LIMIT = 200;
4282
+ var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,255}$/;
4283
+ var TERMINAL_STATUSES2 = new Set(TERMINAL_SESSION_RUN_STATUSES);
4284
+ var ACTIVE_STATUSES = new Set([
4285
+ "created",
4286
+ "running",
4287
+ "waiting_approval",
4288
+ "cancel_requested"
4289
+ ]);
4290
+ var STATUS_TRANSITIONS = {
4291
+ created: new Set(["created", "running", "waiting_approval", "completed", "failed", "cancelled", "timed_out"]),
4292
+ running: new Set(["running", "waiting_approval", "cancel_requested", "completed", "failed", "cancelled", "timed_out"]),
4293
+ waiting_approval: new Set(["waiting_approval", "running", "cancel_requested", "completed", "failed", "cancelled", "timed_out"]),
4294
+ cancel_requested: new Set(["cancel_requested", "cancelled", "failed", "timed_out"]),
4295
+ completed: new Set(["completed"]),
4296
+ failed: new Set(["failed"]),
4297
+ cancelled: new Set(["cancelled"]),
4298
+ timed_out: new Set(["timed_out"])
4299
+ };
4300
+ var metadataRedaction = createRedactionPolicy3({
4301
+ includeEmails: true,
4302
+ rules: [
4303
+ { name: "private-key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
4304
+ { name: "common-provider-key", pattern: /\b(?:sk|ghp|github_pat|xox[baprs])[-_][A-Za-z0-9_-]{8,}\b/gi },
4305
+ { name: "url-credentials", pattern: /:\/\/[^\s/@:]+:[^\s/@]+@/g, replacement: "://[REDACTED]@" }
4306
+ ]
4307
+ });
4308
+ var byteLength = (value) => Buffer.byteLength(value, "utf8");
4309
+ var boundedInteger = (name, value, fallback, minimum) => {
4310
+ const resolved = value ?? fallback;
4311
+ if (!Number.isSafeInteger(resolved) || resolved < minimum) {
4312
+ throw new Error(`${name} must be an integer greater than or equal to ${minimum}.`);
4313
+ }
4314
+ return resolved;
4315
+ };
4316
+ var assertTimestamp = (name, value) => {
4317
+ if (!Number.isSafeInteger(value) || value < 0) {
4318
+ throw new Error(`${name} must be a non-negative integer timestamp.`);
4319
+ }
4320
+ };
4321
+ var assertIdentifier = (name, value) => {
4322
+ if (!ID_PATTERN.test(value)) {
4323
+ throw new Error(`${name} contains unsupported characters or exceeds 256 bytes.`);
4324
+ }
4325
+ if (metadataRedaction.redactText(value) !== value) {
4326
+ throw new Error(`${name} resembles sensitive metadata and cannot be stored in the session index.`);
4327
+ }
4328
+ return value;
4329
+ };
4330
+ var assertScopeSegment = (name, value) => {
4331
+ const normalized = value.trim();
4332
+ if (!normalized || normalized.length > 128 || /[\u0000-\u001f\u007f]/.test(normalized)) {
4333
+ throw new Error(`${name} must contain 1-128 printable characters.`);
4334
+ }
4335
+ return normalized;
4336
+ };
4337
+ var normalizeTitle = (value, maxMetadataBytes) => {
4338
+ if (value === undefined || value === null) {
4339
+ return value;
4340
+ }
4341
+ const normalized = metadataRedaction.redactText(value).replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
4342
+ if (!normalized) {
4343
+ throw new Error("Session title must not be empty.");
4344
+ }
4345
+ if (byteLength(normalized) > maxMetadataBytes) {
4346
+ throw new Error(`Session title exceeds the ${maxMetadataBytes}-byte metadata limit.`);
4347
+ }
4348
+ return normalized;
4349
+ };
4350
+ var normalizeScope = (scope) => {
4351
+ const tenantId = assertScopeSegment("scope.tenantId", scope.tenantId);
4352
+ const userId = scope.userId === undefined ? "" : assertScopeSegment("scope.userId", scope.userId);
4353
+ const namespace = scope.namespace === undefined ? "" : assertScopeSegment("scope.namespace", scope.namespace);
4354
+ return `${tenantId}\x00${userId}\x00${namespace}`;
4355
+ };
4356
+ var stableKey = (kind, value) => createHash8("sha256").update(kind).update("\x00").update(value).digest("hex");
4357
+ var sessionId = () => `ses_${randomUUID4()}`;
4358
+ var turnId = () => `turn_${randomUUID4()}`;
4359
+ var isMissing = (error) => error.code === "ENOENT";
4360
+ var ensurePrivateDatabase = async (workspace, stateDirectory) => {
4361
+ const requestedStateDirectory = path8.resolve(stateDirectory);
4362
+ await validateStateDirectory(workspace, requestedStateDirectory);
4363
+ await mkdir4(requestedStateDirectory, { recursive: true, mode: 448 });
4364
+ const directoryEntry = await lstat6(requestedStateDirectory);
4365
+ if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
4366
+ throw new Error(`The session state directory must be a real directory: ${requestedStateDirectory}.`);
4367
+ }
4368
+ await chmod3(requestedStateDirectory, 448);
4369
+ const canonicalStateDirectory2 = await realpath4(requestedStateDirectory);
4370
+ const databasePath = path8.join(canonicalStateDirectory2, HARNESS_SESSION_INDEX_FILE);
4371
+ let databaseEntry;
4372
+ try {
4373
+ databaseEntry = await lstat6(databasePath);
4374
+ } catch (error) {
4375
+ if (!isMissing(error)) {
4376
+ throw error;
4377
+ }
4378
+ try {
4379
+ const handle = await open3(databasePath, "wx", 384);
4380
+ await handle.close();
4381
+ } catch (creationError) {
4382
+ if (creationError.code !== "EEXIST") {
4383
+ throw creationError;
4384
+ }
4385
+ }
4386
+ databaseEntry = await lstat6(databasePath);
4387
+ }
4388
+ if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
4389
+ throw new Error(`The session SQLite path must be a real file: ${databasePath}.`);
4390
+ }
4391
+ await chmod3(databasePath, 384);
4392
+ return { databasePath, databaseEntry };
4393
+ };
4394
+ var toRunReference = (row) => ({
4395
+ turnId: row.turn_id,
4396
+ ...row.source_turn_id ? { sourceTurnId: row.source_turn_id } : {},
4397
+ sequence: row.ordinal,
4398
+ runId: row.run_id,
4399
+ provider: row.provider,
4400
+ model: row.model,
4401
+ ...row.role ? { role: row.role } : {},
4402
+ status: row.status,
4403
+ createdAt: row.created_at,
4404
+ updatedAt: row.updated_at,
4405
+ ...row.completed_at === null ? {} : { completedAt: row.completed_at }
4406
+ });
4407
+ var metadataSize = (input) => Object.values(input).reduce((total, value) => total + (typeof value === "string" ? byteLength(value) : 0), 0);
4408
+ var isTerminal = (status) => TERMINAL_STATUSES2.has(status);
4409
+ var openCliSessionStore = async (options) => {
4410
+ const workspace = await realpath4(path8.resolve(options.workspace));
4411
+ const workspaceEntry = await stat3(workspace);
4412
+ if (!workspaceEntry.isDirectory()) {
4413
+ throw new Error(`The session workspace is not a directory: ${workspace}.`);
4414
+ }
4415
+ const scopeValue2 = normalizeScope(options.scope);
4416
+ const workspaceKey = stableKey("workspace", workspace);
4417
+ const scopeKey = stableKey("scope", scopeValue2);
4418
+ const retentionMs = boundedInteger("retentionMs", options.retentionMs, DEFAULT_RETENTION_MS, 0);
4419
+ const maxSessions = boundedInteger("maxSessionsPerWorkspace", options.maxSessionsPerWorkspace, DEFAULT_MAX_SESSIONS, 1);
4420
+ const maxRuns = boundedInteger("maxRunsPerSession", options.maxRunsPerSession, DEFAULT_MAX_RUNS, 1);
4421
+ const maxMetadataBytes = boundedInteger("maxMetadataBytes", options.maxMetadataBytes, DEFAULT_MAX_METADATA_BYTES, 32);
4422
+ const maxIndexBytes = boundedInteger("maxIndexBytes", options.maxIndexBytes, DEFAULT_MAX_INDEX_BYTES, 1024);
4423
+ const now = options.now ?? Date.now;
4424
+ const { databasePath, databaseEntry } = await ensurePrivateDatabase(workspace, options.stateDirectory);
4425
+ const database = new Database2(databasePath, { create: false, strict: true });
4426
+ let closed = false;
4427
+ const assertOpen = () => {
4428
+ if (closed) {
4429
+ throw new Error("The CLI session store is closed.");
4430
+ }
4431
+ };
4432
+ const transaction = (operation) => {
4433
+ assertOpen();
4434
+ database.exec("BEGIN IMMEDIATE");
4435
+ try {
4436
+ const result = operation();
4437
+ const size = database.query(`
4438
+ SELECT
4439
+ COALESCE((SELECT SUM(metadata_bytes) FROM zhivex_cli_sessions WHERE workspace_key = ?1 AND scope_key = ?2), 0) +
4440
+ 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)
4441
+ AS size
4442
+ `).get(workspaceKey, scopeKey)?.size ?? 0;
4443
+ if (size > maxIndexBytes) {
4444
+ throw new Error(`Session index metadata exceeds the ${maxIndexBytes}-byte limit.`);
4445
+ }
4446
+ database.exec("COMMIT");
4447
+ return result;
4448
+ } catch (error) {
4449
+ try {
4450
+ database.exec("ROLLBACK");
4451
+ } catch {}
4452
+ throw error;
4453
+ }
4454
+ };
4455
+ const ensureRevision = (row, expected) => {
4456
+ if (expected !== undefined && row.revision !== expected) {
4457
+ throw new Error(`Session ${row.session_id} changed concurrently (expected revision ${expected}, found ${row.revision}).`);
4458
+ }
4459
+ };
4460
+ const selectSessionRow = (candidateSessionId, includeDeleted = false) => {
4461
+ assertIdentifier("sessionId", candidateSessionId);
4462
+ return database.query(`
4463
+ SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
4464
+ created_at, updated_at, archived_at, deleted_at
4465
+ FROM zhivex_cli_sessions
4466
+ WHERE session_id = ?1 AND workspace_key = ?2 AND scope_key = ?3
4467
+ ${includeDeleted ? "" : "AND deleted_at IS NULL"}
4468
+ `).get(candidateSessionId, workspaceKey, scopeKey);
4469
+ };
4470
+ const requireSessionRow = (candidateSessionId, includeDeleted = false) => {
4471
+ const row = selectSessionRow(candidateSessionId, includeDeleted);
4472
+ if (!row) {
4473
+ throw new Error(`Session ${candidateSessionId} was not found in this workspace and scope.`);
4474
+ }
4475
+ return row;
4476
+ };
4477
+ const selectRuns = (candidateSessionId) => database.query(`
4478
+ SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
4479
+ created_at, updated_at, completed_at
4480
+ FROM zhivex_cli_session_runs
4481
+ WHERE session_id = ?1
4482
+ ORDER BY ordinal ASC
4483
+ `).all(candidateSessionId).map(toRunReference);
4484
+ const nextActivitySequence = () => database.query(`
4485
+ SELECT COALESCE(MAX(activity_seq), 0) + 1 AS count
4486
+ FROM zhivex_cli_sessions
4487
+ WHERE workspace_key = ?1 AND scope_key = ?2
4488
+ `).get(workspaceKey, scopeKey)?.count ?? 1;
4489
+ const sessionBase = (row) => ({
4490
+ schemaVersion: HARNESS_SESSION_SCHEMA_VERSION,
4491
+ sessionId: row.session_id,
4492
+ workspaceKey,
4493
+ scopeKey,
4494
+ ...row.title ? { title: row.title } : {},
4495
+ ...row.parent_session_id ? { parentSessionId: row.parent_session_id } : {},
4496
+ ...row.forked_from_turn_id ? { forkedFromTurnId: row.forked_from_turn_id } : {},
4497
+ revision: row.revision,
4498
+ createdAt: row.created_at,
4499
+ updatedAt: row.updated_at,
4500
+ ...row.archived_at === null ? {} : { archivedAt: row.archived_at },
4501
+ ...row.deleted_at === null ? {} : { deletedAt: row.deleted_at }
4502
+ });
4503
+ const hydrate = (row) => ({
4504
+ ...sessionBase(row),
4505
+ runs: selectRuns(row.session_id)
4506
+ });
4507
+ const latestRunRow = (candidateSessionId) => database.query(`
4508
+ SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
4509
+ created_at, updated_at, completed_at
4510
+ FROM zhivex_cli_session_runs
4511
+ WHERE session_id = ?1
4512
+ ORDER BY ordinal DESC
4513
+ LIMIT 1
4514
+ `).get(candidateSessionId);
4515
+ const assertNoActiveRun = (candidateSessionId, action) => {
4516
+ const latest = latestRunRow(candidateSessionId);
4517
+ if (latest && ACTIVE_STATUSES.has(latest.status)) {
4518
+ throw new Error(`Cannot ${action} session ${candidateSessionId} while run ${latest.run_id} is ${latest.status}.`);
4519
+ }
4520
+ };
4521
+ const insertRun = (candidateSessionId, input, sequence, sourceTurnId) => {
4522
+ const runId = assertIdentifier("runId", input.runId);
4523
+ const provider = assertIdentifier("provider", input.provider);
4524
+ const model = assertIdentifier("model", input.model);
4525
+ const role = input.role === undefined ? null : assertIdentifier("role", input.role);
4526
+ const status = input.status ?? "created";
4527
+ if (!SESSION_RUN_STATUSES.includes(status)) {
4528
+ throw new Error(`Unsupported session run status: ${String(status)}.`);
4529
+ }
4530
+ const createdAt = input.createdAt ?? now();
4531
+ assertTimestamp("createdAt", createdAt);
4532
+ const entryBytes = metadataSize({ runId, provider, model, role });
4533
+ if (entryBytes > maxMetadataBytes) {
4534
+ throw new Error(`Run reference metadata exceeds the ${maxMetadataBytes}-byte limit.`);
4535
+ }
4536
+ const candidateTurnId = turnId();
4537
+ database.query(`
4538
+ INSERT INTO zhivex_cli_session_runs (
4539
+ turn_id, source_turn_id, session_id, ordinal, run_id, provider, model, role,
4540
+ status, created_at, updated_at, completed_at, metadata_bytes
4541
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10, ?11, ?12)
4542
+ `).run(candidateTurnId, sourceTurnId ?? null, candidateSessionId, sequence, runId, provider, model, role, status, createdAt, isTerminal(status) ? createdAt : null, entryBytes);
4543
+ };
4544
+ try {
4545
+ const openedEntry = await lstat6(databasePath);
4546
+ if (openedEntry.isSymbolicLink() || !openedEntry.isFile() || openedEntry.dev !== databaseEntry.dev || openedEntry.ino !== databaseEntry.ino) {
4547
+ throw new Error(`The session SQLite path changed while it was being opened: ${databasePath}.`);
4548
+ }
4549
+ database.exec("PRAGMA journal_mode = WAL");
4550
+ database.exec("PRAGMA busy_timeout = 5000");
4551
+ database.exec("PRAGMA foreign_keys = ON");
4552
+ database.exec(`
4553
+ CREATE TABLE IF NOT EXISTS zhivex_cli_session_schema (
4554
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
4555
+ version INTEGER NOT NULL
4556
+ );
4557
+ INSERT OR IGNORE INTO zhivex_cli_session_schema (singleton, version)
4558
+ VALUES (1, ${HARNESS_SESSION_SCHEMA_VERSION});
4559
+ CREATE TABLE IF NOT EXISTS zhivex_cli_sessions (
4560
+ session_id TEXT PRIMARY KEY,
4561
+ workspace_key TEXT NOT NULL,
4562
+ scope_key TEXT NOT NULL,
4563
+ title TEXT,
4564
+ parent_session_id TEXT,
4565
+ forked_from_turn_id TEXT,
4566
+ revision INTEGER NOT NULL DEFAULT 0,
4567
+ activity_seq INTEGER NOT NULL,
4568
+ created_at INTEGER NOT NULL,
4569
+ updated_at INTEGER NOT NULL,
4570
+ archived_at INTEGER,
4571
+ deleted_at INTEGER,
4572
+ metadata_bytes INTEGER NOT NULL,
4573
+ CHECK (length(session_id) <= 260),
4574
+ CHECK (metadata_bytes >= 0)
4575
+ );
4576
+ CREATE INDEX IF NOT EXISTS zhivex_cli_sessions_latest
4577
+ ON zhivex_cli_sessions (workspace_key, scope_key, deleted_at, archived_at, activity_seq DESC);
4578
+ CREATE TABLE IF NOT EXISTS zhivex_cli_session_runs (
4579
+ turn_id TEXT PRIMARY KEY,
4580
+ source_turn_id TEXT,
4581
+ session_id TEXT NOT NULL REFERENCES zhivex_cli_sessions(session_id) ON DELETE CASCADE,
4582
+ ordinal INTEGER NOT NULL,
4583
+ run_id TEXT NOT NULL,
4584
+ provider TEXT NOT NULL,
4585
+ model TEXT NOT NULL,
4586
+ role TEXT,
4587
+ status TEXT NOT NULL CHECK (status IN ('created', 'running', 'waiting_approval', 'cancel_requested', 'completed', 'failed', 'cancelled', 'timed_out')),
4588
+ created_at INTEGER NOT NULL,
4589
+ updated_at INTEGER NOT NULL,
4590
+ completed_at INTEGER,
4591
+ metadata_bytes INTEGER NOT NULL,
4592
+ UNIQUE (session_id, ordinal),
4593
+ UNIQUE (session_id, run_id),
4594
+ CHECK (ordinal >= 0),
4595
+ CHECK (metadata_bytes >= 0)
4596
+ );
4597
+ CREATE INDEX IF NOT EXISTS zhivex_cli_session_runs_latest
4598
+ ON zhivex_cli_session_runs (session_id, ordinal DESC);
4599
+ `);
4600
+ const schemaVersion = database.query("SELECT version FROM zhivex_cli_session_schema WHERE singleton = 1").get()?.version;
4601
+ if (schemaVersion !== HARNESS_SESSION_SCHEMA_VERSION) {
4602
+ throw new Error(`Unsupported CLI session schema version: ${String(schemaVersion)}.`);
4603
+ }
4604
+ await chmod3(databasePath, 384);
4605
+ } catch (error) {
4606
+ database.close(false);
4607
+ throw error;
4608
+ }
4609
+ const prune = async (input = {}) => transaction(() => {
4610
+ const before = input.before ?? Math.max(0, now() - retentionMs);
4611
+ assertTimestamp("before", before);
4612
+ const deletedSessions = database.query(`
4613
+ SELECT COUNT(*) AS count FROM zhivex_cli_sessions
4614
+ WHERE workspace_key = ?1 AND scope_key = ?2
4615
+ AND updated_at < ?3
4616
+ AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
4617
+ `).get(workspaceKey, scopeKey, before)?.count ?? 0;
4618
+ database.query(`
4619
+ DELETE FROM zhivex_cli_sessions
4620
+ WHERE workspace_key = ?1 AND scope_key = ?2
4621
+ AND updated_at < ?3
4622
+ AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
4623
+ `).run(workspaceKey, scopeKey, before);
4624
+ return { before, deletedSessions };
4625
+ });
4626
+ const store = {
4627
+ databasePath,
4628
+ workspaceKey,
4629
+ scopeKey,
4630
+ async create(input = {}) {
4631
+ return transaction(() => {
4632
+ const cutoff = Math.max(0, now() - retentionMs);
4633
+ database.query(`
4634
+ DELETE FROM zhivex_cli_sessions
4635
+ WHERE workspace_key = ?1 AND scope_key = ?2 AND updated_at < ?3
4636
+ AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
4637
+ `).run(workspaceKey, scopeKey, cutoff);
4638
+ const count = database.query(`
4639
+ SELECT COUNT(*) AS count FROM zhivex_cli_sessions
4640
+ WHERE workspace_key = ?1 AND scope_key = ?2
4641
+ `).get(workspaceKey, scopeKey)?.count ?? 0;
4642
+ if (count >= maxSessions) {
4643
+ throw new Error(`Session limit reached for this workspace and scope (${maxSessions}). Archive and prune an older session.`);
4644
+ }
4645
+ const createdAt = now();
4646
+ assertTimestamp("now", createdAt);
4647
+ const title = normalizeTitle(input.title, maxMetadataBytes) ?? null;
4648
+ const candidateSessionId = sessionId();
4649
+ const activitySequence = nextActivitySequence();
4650
+ database.query(`
4651
+ INSERT INTO zhivex_cli_sessions (
4652
+ session_id, workspace_key, scope_key, title, revision, created_at,
4653
+ updated_at, activity_seq, metadata_bytes
4654
+ ) VALUES (?1, ?2, ?3, ?4, 0, ?5, ?5, ?6, ?7)
4655
+ `).run(candidateSessionId, workspaceKey, scopeKey, title, createdAt, activitySequence, metadataSize({ title }));
4656
+ if (input.initialRun) {
4657
+ insertRun(candidateSessionId, input.initialRun, 0);
4658
+ }
4659
+ return hydrate(requireSessionRow(candidateSessionId));
4660
+ });
4661
+ },
4662
+ async get(candidateSessionId, input = {}) {
4663
+ assertOpen();
4664
+ const row = selectSessionRow(candidateSessionId, input.includeDeleted ?? false);
4665
+ return row ? hydrate(row) : undefined;
4666
+ },
4667
+ async list(query = {}) {
4668
+ assertOpen();
4669
+ const limit = boundedInteger("limit", query.limit, 50, 1);
4670
+ if (limit > MAX_LIST_LIMIT) {
4671
+ throw new Error(`limit cannot exceed ${MAX_LIST_LIMIT}.`);
4672
+ }
4673
+ const rows = database.query(`
4674
+ SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
4675
+ created_at, updated_at, archived_at, deleted_at
4676
+ FROM zhivex_cli_sessions
4677
+ WHERE workspace_key = ?1 AND scope_key = ?2
4678
+ ${query.includeArchived ? "" : "AND archived_at IS NULL"}
4679
+ ${query.includeDeleted ? "" : "AND deleted_at IS NULL"}
4680
+ ORDER BY activity_seq DESC
4681
+ LIMIT ?3
4682
+ `).all(workspaceKey, scopeKey, limit);
4683
+ return rows.map((row) => {
4684
+ const latest = latestRunRow(row.session_id);
4685
+ const runCount = database.query("SELECT COUNT(*) AS count FROM zhivex_cli_session_runs WHERE session_id = ?1").get(row.session_id)?.count ?? 0;
4686
+ return {
4687
+ ...sessionBase(row),
4688
+ runCount,
4689
+ ...latest ? { latestRun: toRunReference(latest) } : {}
4690
+ };
4691
+ });
4692
+ },
4693
+ async latest(input = {}) {
4694
+ assertOpen();
4695
+ const row = database.query(`
4696
+ SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
4697
+ created_at, updated_at, archived_at, deleted_at
4698
+ FROM zhivex_cli_sessions
4699
+ WHERE workspace_key = ?1 AND scope_key = ?2 AND deleted_at IS NULL
4700
+ ${input.includeArchived ? "" : "AND archived_at IS NULL"}
4701
+ ORDER BY activity_seq DESC
4702
+ LIMIT 1
4703
+ `).get(workspaceKey, scopeKey);
4704
+ return row ? hydrate(row) : undefined;
4705
+ },
4706
+ async update(candidateSessionId, input) {
4707
+ return transaction(() => {
4708
+ const row = requireSessionRow(candidateSessionId);
4709
+ ensureRevision(row, input.expectedRevision);
4710
+ if (input.archived === true) {
4711
+ assertNoActiveRun(candidateSessionId, "archive");
4712
+ }
4713
+ const title = input.title === undefined ? row.title : normalizeTitle(input.title, maxMetadataBytes) ?? null;
4714
+ const updatedAt = now();
4715
+ assertTimestamp("now", updatedAt);
4716
+ const archivedAt = input.archived === undefined ? row.archived_at : input.archived ? updatedAt : null;
4717
+ const activitySequence = nextActivitySequence();
4718
+ database.query(`
4719
+ UPDATE zhivex_cli_sessions
4720
+ SET title = ?1, archived_at = ?2, updated_at = ?3, revision = revision + 1,
4721
+ metadata_bytes = ?4, activity_seq = ?5
4722
+ WHERE session_id = ?6 AND workspace_key = ?7 AND scope_key = ?8 AND revision = ?9
4723
+ `).run(title, archivedAt, updatedAt, metadataSize({ title }), activitySequence, candidateSessionId, workspaceKey, scopeKey, row.revision);
4724
+ return hydrate(requireSessionRow(candidateSessionId));
4725
+ });
4726
+ },
4727
+ async rename(candidateSessionId, title, input = {}) {
4728
+ return store.update(candidateSessionId, {
4729
+ title,
4730
+ ...input.expectedRevision === undefined ? {} : { expectedRevision: input.expectedRevision }
4731
+ });
4732
+ },
4733
+ async appendRun(candidateSessionId, input, appendOptions = {}) {
4734
+ return transaction(() => {
4735
+ const row = requireSessionRow(candidateSessionId);
4736
+ ensureRevision(row, appendOptions.expectedRevision);
4737
+ if (row.archived_at !== null) {
4738
+ throw new Error(`Cannot append a run to archived session ${candidateSessionId}.`);
4739
+ }
4740
+ const existing = database.query(`
4741
+ SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
4742
+ created_at, updated_at, completed_at
4743
+ FROM zhivex_cli_session_runs WHERE session_id = ?1 AND run_id = ?2
4744
+ `).get(candidateSessionId, input.runId);
4745
+ if (existing) {
4746
+ const requestedStatus = input.status ?? "created";
4747
+ if (existing.provider === input.provider && existing.model === input.model && (existing.role ?? undefined) === input.role && existing.status === requestedStatus) {
4748
+ return hydrate(row);
4749
+ }
4750
+ throw new Error(`Run ${input.runId} is already immutably bound to ${existing.provider}/${existing.model} in this session.`);
4751
+ }
4752
+ assertNoActiveRun(candidateSessionId, "start another run in");
4753
+ const count = database.query("SELECT COUNT(*) AS count FROM zhivex_cli_session_runs WHERE session_id = ?1").get(candidateSessionId)?.count ?? 0;
4754
+ if (count >= maxRuns) {
4755
+ throw new Error(`Run-reference limit reached for session ${candidateSessionId} (${maxRuns}).`);
4756
+ }
4757
+ insertRun(candidateSessionId, input, count);
4758
+ const updatedAt = now();
4759
+ assertTimestamp("now", updatedAt);
4760
+ const activitySequence = nextActivitySequence();
4761
+ database.query(`
4762
+ UPDATE zhivex_cli_sessions SET updated_at = ?1, activity_seq = ?2, revision = revision + 1
4763
+ WHERE session_id = ?3 AND revision = ?4
4764
+ `).run(updatedAt, activitySequence, candidateSessionId, row.revision);
4765
+ return hydrate(requireSessionRow(candidateSessionId));
4766
+ });
4767
+ },
4768
+ async updateRun(candidateSessionId, runId, input) {
4769
+ return transaction(() => {
4770
+ const session = requireSessionRow(candidateSessionId);
4771
+ ensureRevision(session, input.expectedSessionRevision);
4772
+ assertIdentifier("runId", runId);
4773
+ const run = database.query(`
4774
+ SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
4775
+ created_at, updated_at, completed_at
4776
+ FROM zhivex_cli_session_runs WHERE session_id = ?1 AND run_id = ?2
4777
+ `).get(candidateSessionId, runId);
4778
+ if (!run) {
4779
+ throw new Error(`Run ${runId} is not linked to session ${candidateSessionId}.`);
4780
+ }
4781
+ if (!STATUS_TRANSITIONS[run.status].has(input.status)) {
4782
+ throw new Error(`Run ${runId} cannot transition from ${run.status} to ${input.status}.`);
4783
+ }
4784
+ const updatedAt = input.updatedAt ?? now();
4785
+ assertTimestamp("updatedAt", updatedAt);
4786
+ if (updatedAt < run.updated_at) {
4787
+ throw new Error(`Run ${runId} update timestamp cannot move backwards.`);
4788
+ }
4789
+ const completedAt = isTerminal(input.status) ? input.completedAt ?? updatedAt : null;
4790
+ if (completedAt !== null) {
4791
+ assertTimestamp("completedAt", completedAt);
4792
+ if (completedAt < run.created_at || completedAt > updatedAt) {
4793
+ throw new Error("completedAt must be between the run creation and update timestamps.");
4794
+ }
4795
+ }
4796
+ database.query(`
4797
+ UPDATE zhivex_cli_session_runs
4798
+ SET status = ?1, updated_at = ?2, completed_at = ?3
4799
+ WHERE session_id = ?4 AND run_id = ?5
4800
+ `).run(input.status, updatedAt, completedAt, candidateSessionId, runId);
4801
+ const activitySequence = nextActivitySequence();
4802
+ database.query(`
4803
+ UPDATE zhivex_cli_sessions SET updated_at = ?1, activity_seq = ?2, revision = revision + 1
4804
+ WHERE session_id = ?3 AND revision = ?4
4805
+ `).run(updatedAt, activitySequence, candidateSessionId, session.revision);
4806
+ return hydrate(requireSessionRow(candidateSessionId));
4807
+ });
4808
+ },
4809
+ async fork(candidateSessionId, input = {}) {
4810
+ return transaction(() => {
4811
+ const source = requireSessionRow(candidateSessionId);
4812
+ ensureRevision(source, input.expectedRevision);
4813
+ const sourceRuns = selectRuns(candidateSessionId);
4814
+ let forkRuns = sourceRuns;
4815
+ let forkedFromTurnId;
4816
+ if (input.atTurnId) {
4817
+ const index = sourceRuns.findIndex((run) => run.turnId === input.atTurnId);
4818
+ if (index < 0) {
4819
+ throw new Error(`Turn ${input.atTurnId} is not part of session ${candidateSessionId}.`);
4820
+ }
4821
+ forkRuns = sourceRuns.slice(0, index + 1);
4822
+ forkedFromTurnId = input.atTurnId;
4823
+ } else if (sourceRuns.length > 0) {
4824
+ forkedFromTurnId = sourceRuns.at(-1).turnId;
4825
+ }
4826
+ const branchPoint = forkRuns.at(-1);
4827
+ if (branchPoint && !isTerminal(branchPoint.status)) {
4828
+ throw new Error(`Cannot fork from non-terminal run ${branchPoint.runId} (${branchPoint.status}).`);
4829
+ }
4830
+ if (forkRuns.length > maxRuns) {
4831
+ throw new Error(`Fork history exceeds the ${maxRuns}-run limit.`);
4832
+ }
4833
+ const count = database.query(`
4834
+ SELECT COUNT(*) AS count FROM zhivex_cli_sessions
4835
+ WHERE workspace_key = ?1 AND scope_key = ?2
4836
+ `).get(workspaceKey, scopeKey)?.count ?? 0;
4837
+ if (count >= maxSessions) {
4838
+ throw new Error(`Session limit reached for this workspace and scope (${maxSessions}).`);
4839
+ }
4840
+ const createdAt = now();
4841
+ assertTimestamp("now", createdAt);
4842
+ const title = normalizeTitle(input.title, maxMetadataBytes) ?? source.title;
4843
+ const forkSessionId = sessionId();
4844
+ const activitySequence = nextActivitySequence();
4845
+ database.query(`
4846
+ INSERT INTO zhivex_cli_sessions (
4847
+ session_id, workspace_key, scope_key, title, parent_session_id,
4848
+ forked_from_turn_id, revision, created_at, updated_at, activity_seq, metadata_bytes
4849
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7, ?7, ?8, ?9)
4850
+ `).run(forkSessionId, workspaceKey, scopeKey, title, candidateSessionId, forkedFromTurnId ?? null, createdAt, activitySequence, metadataSize({ title }));
4851
+ for (const [index, run] of forkRuns.entries()) {
4852
+ insertRun(forkSessionId, {
4853
+ runId: run.runId,
4854
+ provider: run.provider,
4855
+ model: run.model,
4856
+ ...run.role ? { role: run.role } : {},
4857
+ status: run.status,
4858
+ createdAt: run.createdAt
4859
+ }, index, run.turnId);
4860
+ database.query(`
4861
+ UPDATE zhivex_cli_session_runs
4862
+ SET updated_at = ?1, completed_at = ?2
4863
+ WHERE session_id = ?3 AND ordinal = ?4
4864
+ `).run(run.updatedAt, run.completedAt ?? null, forkSessionId, index);
4865
+ }
4866
+ return hydrate(requireSessionRow(forkSessionId));
4867
+ });
4868
+ },
4869
+ async archive(candidateSessionId, input = {}) {
4870
+ return store.update(candidateSessionId, {
4871
+ archived: true,
4872
+ ...input.expectedRevision === undefined ? {} : { expectedRevision: input.expectedRevision }
4873
+ });
4874
+ },
4875
+ async delete(candidateSessionId, input = {}) {
4876
+ return transaction(() => {
4877
+ const row = requireSessionRow(candidateSessionId);
4878
+ ensureRevision(row, input.expectedRevision);
4879
+ assertNoActiveRun(candidateSessionId, "delete");
4880
+ const deletedAt = now();
4881
+ assertTimestamp("now", deletedAt);
4882
+ const activitySequence = nextActivitySequence();
4883
+ database.query(`
4884
+ UPDATE zhivex_cli_sessions
4885
+ SET deleted_at = ?1, updated_at = ?1, activity_seq = ?2, revision = revision + 1
4886
+ WHERE session_id = ?3 AND revision = ?4
4887
+ `).run(deletedAt, activitySequence, candidateSessionId, row.revision);
4888
+ return hydrate(requireSessionRow(candidateSessionId, true));
4889
+ });
4890
+ },
4891
+ prune,
4892
+ close() {
4893
+ if (!closed) {
4894
+ closed = true;
4895
+ database.close(false);
4896
+ }
4897
+ }
4898
+ };
4899
+ return store;
4900
+ };
4901
+ var openSessionStore = openCliSessionStore;
4902
+
4903
+ // src/cli.ts
4904
+ var HARNESS_RESUME_METADATA_KEY = "zhivexHarnessResume";
4905
+ var HARNESS_RESUME_METADATA_SCHEMA_VERSION = 1;
4906
+ var CLI_EXIT_CODES = {
4907
+ success: 0,
4908
+ runtimeError: 1,
4909
+ usageError: 2,
4910
+ doctorFailed: 3
4911
+ };
4912
+ var COMMANDS = new Set(["run", "review", "chat", "providers", "doctor", "resume", "runs", "sessions", "help", "version"]);
4913
+ var RUNS_COMMANDS = new Set(["list", "inspect", "cancel", "cleanup", "export"]);
4914
+ var SESSIONS_COMMANDS = new Set(["list", "inspect", "rename", "fork", "archive"]);
4915
+ var RUN_STATUSES = new Set([
4916
+ "queued",
4917
+ "running",
4918
+ "completed",
4919
+ "suspended",
4920
+ "waiting_approval",
4921
+ "cancel_requested",
4922
+ "failed",
4923
+ "cancelled",
4924
+ "timed_out"
4925
+ ]);
4926
+
4927
+ class CliUsageError extends Error {
4928
+ constructor(message) {
4929
+ super(message);
4930
+ this.name = "CliUsageError";
3892
4931
  }
3893
4932
  }
3894
4933
  var harnessConfigInput = (config) => ({
@@ -3943,13 +4982,14 @@ var harnessConfigInput = (config) => ({
3943
4982
  } : {},
3944
4983
  ...config.mcpConfigPath ? { mcpConfigPath: config.mcpConfigPath } : {}
3945
4984
  });
3946
- var createHarnessResumeMetadata = (config) => ({
4985
+ var createHarnessResumeMetadata = (config, routes) => ({
3947
4986
  [HARNESS_RESUME_METADATA_KEY]: JSON.parse(JSON.stringify({
3948
4987
  schemaVersion: HARNESS_RESUME_METADATA_SCHEMA_VERSION,
3949
- config
4988
+ config,
4989
+ ...routes && routes.size > 0 ? { routes: serializeHarnessModelRoutes(routes) } : {}
3950
4990
  }))
3951
4991
  });
3952
- var readHarnessResumeConfig = (state) => {
4992
+ var readHarnessResumeDocument = (state) => {
3953
4993
  const value = state.metadata?.[HARNESS_RESUME_METADATA_KEY];
3954
4994
  if (value === undefined)
3955
4995
  return;
@@ -3960,12 +5000,39 @@ var readHarnessResumeConfig = (state) => {
3960
5000
  if (document.schemaVersion !== HARNESS_RESUME_METADATA_SCHEMA_VERSION || !document.config || typeof document.config !== "object" || Array.isArray(document.config)) {
3961
5001
  throw new Error("Persisted harness resume metadata is invalid.");
3962
5002
  }
5003
+ return document;
5004
+ };
5005
+ var readHarnessResumeConfig = (state) => {
5006
+ const document = readHarnessResumeDocument(state);
5007
+ if (!document)
5008
+ return;
3963
5009
  try {
3964
5010
  return harnessConfigInput(document.config);
3965
5011
  } catch {
3966
5012
  throw new Error("Persisted harness resume metadata is invalid.");
3967
5013
  }
3968
5014
  };
5015
+ var readHarnessResumeRoutes = (state) => {
5016
+ const document = readHarnessResumeDocument(state);
5017
+ if (!document?.routes)
5018
+ return resolveHarnessModelRoutes();
5019
+ if (typeof document.routes !== "object" || Array.isArray(document.routes)) {
5020
+ throw new Error("Persisted harness routing metadata is invalid.");
5021
+ }
5022
+ try {
5023
+ const values = Object.entries(document.routes).map(([profile, target]) => {
5024
+ if (!target || typeof target !== "object" || Array.isArray(target))
5025
+ throw new Error("invalid");
5026
+ const route = target;
5027
+ if (typeof route.provider !== "string" || typeof route.model !== "string")
5028
+ throw new Error("invalid");
5029
+ return `${profile}=${route.provider}:${route.model}`;
5030
+ });
5031
+ return resolveHarnessModelRoutes(values);
5032
+ } catch {
5033
+ throw new Error("Persisted harness routing metadata is invalid.");
5034
+ }
5035
+ };
3969
5036
  var shellArgument = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
3970
5037
  var resumeCommand = (runId, config, approve = true) => [
3971
5038
  "zhivex-harness",
@@ -3994,7 +5061,16 @@ var parseCliArgs = (argv) => {
3994
5061
  let command = "run";
3995
5062
  let commandWasExplicit = false;
3996
5063
  const positional = [];
3997
- const options = { command, cascade: false, final: false, yes: false, json: false };
5064
+ const options = {
5065
+ command,
5066
+ cascade: false,
5067
+ final: false,
5068
+ yes: false,
5069
+ json: false,
5070
+ jsonl: false,
5071
+ continueSession: false,
5072
+ implicitCommand: true
5073
+ };
3998
5074
  let positionalOnly = false;
3999
5075
  for (let index = 0;index < argv.length; index += 1) {
4000
5076
  const argument = argv[index];
@@ -4028,6 +5104,21 @@ var parseCliArgs = (argv) => {
4028
5104
  options.model = optionValue(argv, index, argument);
4029
5105
  index += 1;
4030
5106
  break;
5107
+ case "--route": {
5108
+ const value = optionValue(argv, index, argument);
5109
+ try {
5110
+ parseHarnessModelRoute(value);
5111
+ } catch (error) {
5112
+ throw new CliUsageError(error instanceof Error ? error.message : String(error));
5113
+ }
5114
+ options.routes ??= [];
5115
+ options.routes.push(value);
5116
+ if (options.routes.length > HARNESS_SUBAGENT_PROFILES.length) {
5117
+ throw new CliUsageError(`--route cannot be repeated more than ${HARNESS_SUBAGENT_PROFILES.length} times.`);
5118
+ }
5119
+ index += 1;
5120
+ break;
5121
+ }
4031
5122
  case "--workspace":
4032
5123
  options.workspace = optionValue(argv, index, argument);
4033
5124
  index += 1;
@@ -4251,6 +5342,16 @@ var parseCliArgs = (argv) => {
4251
5342
  case "--json":
4252
5343
  options.json = true;
4253
5344
  break;
5345
+ case "--jsonl":
5346
+ options.jsonl = true;
5347
+ break;
5348
+ case "--session":
5349
+ options.sessionId = optionValue(argv, index, argument);
5350
+ index += 1;
5351
+ break;
5352
+ case "--continue":
5353
+ options.continueSession = true;
5354
+ break;
4254
5355
  case "--status": {
4255
5356
  const value = optionValue(argv, index, argument);
4256
5357
  if (!RUN_STATUSES.has(value)) {
@@ -4312,6 +5413,16 @@ var parseCliArgs = (argv) => {
4312
5413
  positional.push(argument);
4313
5414
  }
4314
5415
  }
5416
+ options.implicitCommand = !commandWasExplicit;
5417
+ if (options.json && options.jsonl) {
5418
+ throw new CliUsageError("You cannot combine --json and --jsonl.");
5419
+ }
5420
+ if (options.sessionId && options.continueSession) {
5421
+ throw new CliUsageError("You cannot combine --session and --continue.");
5422
+ }
5423
+ if (options.jsonl && options.command !== "run" && options.command !== "resume") {
5424
+ throw new CliUsageError("--jsonl is supported by run and resume.");
5425
+ }
4315
5426
  if (options.command === "resume") {
4316
5427
  const runId = positional.shift();
4317
5428
  if (runId) {
@@ -4339,6 +5450,28 @@ var parseCliArgs = (argv) => {
4339
5450
  if (positional.length > 0) {
4340
5451
  throw new CliUsageError(`runs ${runsCommand} received unexpected positional arguments.`);
4341
5452
  }
5453
+ } else if (options.command === "sessions") {
5454
+ const sessionsCommand = positional.shift();
5455
+ if (!sessionsCommand || !SESSIONS_COMMANDS.has(sessionsCommand)) {
5456
+ throw new CliUsageError("sessions requires one of: list, inspect, rename, fork, archive.");
5457
+ }
5458
+ options.sessionsCommand = sessionsCommand;
5459
+ if (sessionsCommand !== "list") {
5460
+ const sessionId2 = positional.shift();
5461
+ if (!sessionId2)
5462
+ throw new CliUsageError(`sessions ${sessionsCommand} requires a sessionId.`);
5463
+ options.sessionId = sessionId2;
5464
+ }
5465
+ if (sessionsCommand === "rename") {
5466
+ const title = positional.join(" ").trim();
5467
+ if (!title)
5468
+ throw new CliUsageError("sessions rename requires a title.");
5469
+ options.sessionTitle = title;
5470
+ positional.length = 0;
5471
+ }
5472
+ if (positional.length > 0) {
5473
+ throw new CliUsageError(`sessions ${sessionsCommand} received unexpected positional arguments.`);
5474
+ }
4342
5475
  } else if (options.command === "run" || options.command === "review") {
4343
5476
  const prompt = positional.join(" ").trim();
4344
5477
  if (prompt) {
@@ -4352,21 +5485,24 @@ var parseCliArgs = (argv) => {
4352
5485
  var help = `Zhivex Harness v${HARNESS_VERSION}
4353
5486
 
4354
5487
  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]
5488
+ zhx Start the interactive console
5489
+ zhx run [options] "task"
5490
+ zhx review [options] "review task"
5491
+ zhx chat [options] [--continue|--session <id>]
5492
+ zhx providers [--json]
5493
+ zhx doctor [options] [--json]
5494
+ zhx resume [options] <runId> --approve|--deny
5495
+ zhx runs list [--status <status>] [--limit <n>] [--json]
5496
+ zhx sessions list|inspect|rename|fork|archive
5497
+
5498
+ The long command zhivex-harness remains supported for compatibility.
4366
5499
 
4367
5500
  Options:
4368
- --provider <meta|qwen|openai> Provider (default: openai)
5501
+ --provider <${PROVIDERS.join("|")}> Provider (default: openai)
4369
5502
  --model <id> Override the default model
5503
+ --route <role=provider[:model]> Route a subagent role; repeatable
5504
+ --session <id> Open a durable interactive session
5505
+ --continue Open the latest durable interactive session
4370
5506
  --workspace <path> Target workspace (default: cwd)
4371
5507
  --state-dir <path> Durable run-state directory
4372
5508
  --mcp-config <path> Declarative governed MCP JSON configuration
@@ -4408,6 +5544,7 @@ Options:
4408
5544
  --max-parallel-reviews <1-4> Review group concurrency ceiling (default: 2)
4409
5545
  --yes Automatically approve writes and checks
4410
5546
  --json Emit structured final output
5547
+ --jsonl Stream redacted JSON Lines, then the final result
4411
5548
  -h, --help Show this help
4412
5549
  -v, --version Show the version
4413
5550
 
@@ -4420,7 +5557,8 @@ Exit codes:
4420
5557
  Credentials:
4421
5558
  OpenAI: OPENAI_API_KEY
4422
5559
  Meta: MODEL_API_KEY
4423
- Qwen: DASHSCOPE_API_KEY or QWEN_API_KEY`;
5560
+ Qwen: DASHSCOPE_API_KEY or QWEN_API_KEY
5561
+ Gemini: GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY`;
4424
5562
  var summarizeApproval = (approval) => {
4425
5563
  const mustShowCompleteProposal = approval.name === "apply_patch";
4426
5564
  const argumentsText = !mustShowCompleteProposal && approval.arguments.length > 1200 ? `${approval.arguments.slice(0, 1200)}\u2026` : approval.arguments;
@@ -4524,16 +5662,23 @@ var runResultDocument = (result, harness) => ({
4524
5662
  },
4525
5663
  stateDirectory: harness.config.stateDirectory
4526
5664
  });
4527
- var printTerminalResult = (result, harness, json, streamedText) => {
4528
- if (json) {
4529
- process.stdout.write(`${JSON.stringify(runResultDocument(result, harness), null, 2)}
5665
+ var printTerminalResult = (result, harness, output, tracker) => {
5666
+ const document = runResultDocument(result, harness);
5667
+ if (output.jsonl) {
5668
+ tracker.sequence = (tracker.sequence ?? 0) + 1;
5669
+ process.stdout.write(`${serializeStreamResult(document, tracker.sequence)}
4530
5670
  `);
4531
5671
  return;
4532
5672
  }
4533
- if (!streamedText && result.outputText) {
5673
+ if (output.json) {
5674
+ process.stdout.write(`${JSON.stringify(document, null, 2)}
5675
+ `);
5676
+ return;
5677
+ }
5678
+ if (!tracker.streamedText && result.outputText) {
4534
5679
  process.stdout.write(result.outputText);
4535
5680
  }
4536
- if (result.outputText || streamedText) {
5681
+ if (result.outputText || tracker.streamedText) {
4537
5682
  process.stdout.write(`
4538
5683
  `);
4539
5684
  }
@@ -4551,8 +5696,14 @@ ${summarizeApproval(approval)}
4551
5696
  `);
4552
5697
  }
4553
5698
  };
4554
- var streamSink = (json, tracker) => async (event) => {
4555
- if (!json && event.type === "text-delta") {
5699
+ var streamSink = (output, tracker) => async (event) => {
5700
+ if (output.jsonl) {
5701
+ tracker.sequence = (tracker.sequence ?? 0) + 1;
5702
+ process.stdout.write(`${serializeStreamEvent(event, tracker.sequence)}
5703
+ `);
5704
+ return;
5705
+ }
5706
+ if (!output.json && event.type === "text-delta") {
4556
5707
  tracker.streamedText = true;
4557
5708
  process.stdout.write(event.textDelta);
4558
5709
  }
@@ -4570,23 +5721,57 @@ subagent finish \xB7 ${event.childRun.agentId ?? event.childRun.toolName} \xB7 $
4570
5721
  `);
4571
5722
  }
4572
5723
  };
5724
+ var resolvedRouting = (options) => {
5725
+ return resolveHarnessModelRoutes(options.routes);
5726
+ };
5727
+ var assertRoutePricingIsSafe = (routes, costBudget) => {
5728
+ if (routes.size > 0 && costBudget !== undefined) {
5729
+ throw new CliUsageError("Cost budgets cannot be combined with model routes until pricing is configured per role.");
5730
+ }
5731
+ };
5732
+ var withTemporaryHarnessProfiles = async (profiles, configure, operation) => {
5733
+ await configure(profiles);
5734
+ try {
5735
+ return await operation();
5736
+ } finally {
5737
+ await configure([]);
5738
+ }
5739
+ };
5740
+ var createConfiguredHarness = async (options, extraProfiles = [], persistedRoutes) => {
5741
+ const routes = persistedRoutes ?? resolvedRouting(options);
5742
+ const resolvedConfig = resolveHarnessConfig(options);
5743
+ assertRoutePricingIsSafe(routes, resolvedConfig.costBudget);
5744
+ const profiles = [...new Set([
5745
+ ...options.subagentProfiles ?? [],
5746
+ ...extraProfiles,
5747
+ ...routes.keys()
5748
+ ])];
5749
+ const quietTelemetry = options.json || options.jsonl;
5750
+ const harness = await createHarness({
5751
+ ...options,
5752
+ subagentProfiles: profiles,
5753
+ subagentModels: createHarnessRouteModels(routes),
5754
+ onTelemetryEvent: orchestrationObserver(quietTelemetry)
5755
+ });
5756
+ return { harness, routes };
5757
+ };
4573
5758
  var runOnce = async (options) => {
4574
5759
  if (!options.prompt) {
4575
5760
  throw new CliUsageError('Missing task. Example: zhivex-harness run "fix the tests".');
4576
5761
  }
4577
- const harness = await createHarness({ ...options, onTelemetryEvent: orchestrationObserver(options.json) });
5762
+ const { harness, routes } = await createConfiguredHarness(options);
4578
5763
  try {
4579
5764
  const tracker = { streamedText: false };
4580
5765
  const result = await runHarness(harness, {
4581
5766
  prompt: options.prompt,
4582
5767
  scope: harness.config.scope,
4583
- metadata: createHarnessResumeMetadata(harness.config),
5768
+ metadata: createHarnessResumeMetadata(harness.config, routes),
4584
5769
  ...options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}
4585
5770
  }, {
4586
- onEvent: streamSink(options.json, tracker),
5771
+ onEvent: streamSink(options, tracker),
4587
5772
  resolveApprovals: terminalApprovalResolver(options.yes)
4588
5773
  });
4589
- printTerminalResult(result, harness, options.json, tracker.streamedText);
5774
+ printTerminalResult(result, harness, options, tracker);
4590
5775
  if (result.status === "failed" || result.status === "timed_out") {
4591
5776
  process.exitCode = CLI_EXIT_CODES.runtimeError;
4592
5777
  }
@@ -4603,11 +5788,7 @@ var reviewOnce = async (options) => {
4603
5788
  ...options.subagentProfiles ?? [],
4604
5789
  ...requestedReviewers
4605
5790
  ])];
4606
- const harness = await createHarness({
4607
- ...options,
4608
- subagentProfiles: enabledProfiles,
4609
- onTelemetryEvent: orchestrationObserver(options.json)
4610
- });
5791
+ const { harness } = await createConfiguredHarness(options, enabledProfiles);
4611
5792
  try {
4612
5793
  const result = await runHarnessReviewGroup(harness, { prompt: options.prompt, scope: harness.config.scope }, requestedReviewers);
4613
5794
  const document = {
@@ -4678,22 +5859,31 @@ ${summarizeApproval(approval)}
4678
5859
  `);
4679
5860
  }
4680
5861
  const persistedResumeInput = readHarnessResumeConfig(state);
5862
+ const persistedRoutes = readHarnessResumeRoutes(state);
4681
5863
  let persistedResumeOptions = {};
5864
+ let resumeCostBudget = initialConfig.costBudget;
4682
5865
  if (persistedResumeInput) {
4683
5866
  const persistedConfig = resolveHarnessConfig(persistedResumeInput);
4684
5867
  if (persistedConfig.workspace !== initialConfig.workspace || persistedConfig.stateDirectory !== initialConfig.stateDirectory || persistedConfig.storeBackend !== initialConfig.storeBackend || JSON.stringify(persistedConfig.scope) !== JSON.stringify(initialConfig.scope)) {
4685
5868
  throw new Error("Resume locator options do not match the persisted workspace, state directory, store, or scope.");
4686
5869
  }
4687
5870
  persistedResumeOptions = harnessConfigInput(persistedConfig);
5871
+ resumeCostBudget = persistedConfig.costBudget;
4688
5872
  }
5873
+ assertRoutePricingIsSafe(persistedRoutes, resumeCostBudget);
4689
5874
  const harness = await createHarness({
4690
5875
  ...persistedResumeOptions,
4691
5876
  ...options,
4692
5877
  provider: state.provider,
4693
5878
  model: state.modelId,
5879
+ subagentProfiles: [...new Set([
5880
+ ...persistedResumeOptions.subagentProfiles ?? [],
5881
+ ...persistedRoutes.keys()
5882
+ ])],
5883
+ subagentModels: createHarnessRouteModels(persistedRoutes),
4694
5884
  store: persistence.store,
4695
5885
  memory: persistence.memory,
4696
- onTelemetryEvent: orchestrationObserver(options.json)
5886
+ onTelemetryEvent: orchestrationObserver(options.json || options.jsonl)
4697
5887
  });
4698
5888
  const approve = options.approve;
4699
5889
  const tracker = { streamedText: false };
@@ -4701,10 +5891,11 @@ ${summarizeApproval(approval)}
4701
5891
  state,
4702
5892
  approvals: approvalResponses(state.pendingApprovals, approve, approve ? "Approved by resume --approve." : "Denied by resume --deny.")
4703
5893
  }, {
4704
- onEvent: streamSink(options.json, tracker),
5894
+ onEvent: streamSink(options, tracker),
4705
5895
  resolveApprovals: async (approvals) => approvalResponses(approvals, approve, approve ? "Approved by resume --approve." : "Denied by resume --deny.")
4706
5896
  });
4707
- printTerminalResult(result, harness, options.json, tracker.streamedText);
5897
+ await updateIndexedSessionRun(harness.config, result.state.runId, result.status);
5898
+ printTerminalResult(result, harness, options, tracker);
4708
5899
  if (result.status === "failed" || result.status === "timed_out") {
4709
5900
  process.exitCode = CLI_EXIT_CODES.runtimeError;
4710
5901
  }
@@ -4716,11 +5907,87 @@ var chat = async (options) => {
4716
5907
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
4717
5908
  throw new Error("Chat mode requires an interactive terminal. Use run for automation.");
4718
5909
  }
4719
- const harness = await createHarness({ ...options, onTelemetryEvent: orchestrationObserver(false) });
5910
+ const baseConfig = resolveHarnessConfig(options);
5911
+ const sessionStore = await openSessionStoreForConfig(baseConfig);
4720
5912
  const readline = createInterface({ input: process.stdin, output: process.stdout });
5913
+ let runtimeOptions = { ...options };
5914
+ let routes = resolvedRouting(options);
5915
+ const selectedSession = options.sessionId ? await sessionStore.get(options.sessionId) : options.continueSession ? await sessionStore.latest() : undefined;
5916
+ if (options.sessionId && !selectedSession) {
5917
+ sessionStore.close();
5918
+ readline.close();
5919
+ throw new Error(`Session ${options.sessionId} was not found.`);
5920
+ }
5921
+ let session = selectedSession ?? await sessionStore.create();
4721
5922
  let messages = [];
5923
+ const latestState = async (selected) => {
5924
+ const latest = selected.runs.at(-1);
5925
+ if (!latest)
5926
+ return;
5927
+ const persistence = await openHarnessPersistence(baseConfig);
5928
+ try {
5929
+ const state = await persistence.store.load(latest.runId, baseConfig.scope);
5930
+ if (!state)
5931
+ throw new Error(`Run ${latest.runId} referenced by session ${selected.sessionId} was not found.`);
5932
+ return state;
5933
+ } finally {
5934
+ persistence.close();
5935
+ }
5936
+ };
5937
+ let harness;
5938
+ try {
5939
+ const restored = await latestState(session);
5940
+ if (restored) {
5941
+ runtimeOptions = { ...runtimeOptions, provider: restored.provider, model: restored.modelId };
5942
+ routes = readHarnessResumeRoutes(restored);
5943
+ messages = restored.messages;
5944
+ }
5945
+ harness = (await createConfiguredHarness(runtimeOptions, [], routes)).harness;
5946
+ } catch (error) {
5947
+ readline.close();
5948
+ sessionStore.close();
5949
+ throw error;
5950
+ }
5951
+ const refreshSession = async () => {
5952
+ const refreshed = await sessionStore.get(session.sessionId);
5953
+ if (!refreshed)
5954
+ throw new Error(`Session ${session.sessionId} was not found.`);
5955
+ session = refreshed;
5956
+ return session;
5957
+ };
5958
+ const hasActiveTurn = async () => {
5959
+ const latest = (await refreshSession()).runs.at(-1);
5960
+ return latest && !["completed", "failed", "cancelled", "timed_out"].includes(latest.status) ? latest : undefined;
5961
+ };
5962
+ const replaceHarness = async (nextOptions, nextRoutes, extraProfiles = []) => {
5963
+ const created = await createConfiguredHarness(nextOptions, extraProfiles, nextRoutes);
5964
+ harness.close();
5965
+ harness = created.harness;
5966
+ runtimeOptions = nextOptions;
5967
+ routes = new Map(nextRoutes);
5968
+ };
5969
+ const withTemporaryProfiles = async (profiles, operation) => {
5970
+ const normalOptions = runtimeOptions;
5971
+ const normalRoutes = new Map(routes);
5972
+ return withTemporaryHarnessProfiles(profiles, (temporaryProfiles) => replaceHarness(normalOptions, normalRoutes, temporaryProfiles), operation);
5973
+ };
5974
+ const restoreSession = async (selected) => {
5975
+ const state = await latestState(selected);
5976
+ const nextOptions = state ? { ...runtimeOptions, provider: state.provider, model: state.modelId } : runtimeOptions;
5977
+ const nextRoutes = state ? readHarnessResumeRoutes(state) : resolveHarnessModelRoutes();
5978
+ await replaceHarness(nextOptions, nextRoutes);
5979
+ session = selected;
5980
+ messages = state?.messages ?? [];
5981
+ };
5982
+ const statusLine = async () => {
5983
+ const current = await refreshSession();
5984
+ const latest = current.runs.at(-1);
5985
+ const routeText = routes.size === 0 ? "default" : [...routes.values()].map((route) => `${route.profile}=${route.provider}:${route.model}`).join(", ");
5986
+ 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})` : ""}`;
5987
+ };
4722
5988
  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.
5989
+ ` + `session ${session.sessionId}
5990
+ ` + `Type /help for console commands.
4724
5991
  `);
4725
5992
  try {
4726
5993
  for (;; ) {
@@ -4732,39 +5999,253 @@ var chat = async (options) => {
4732
5999
  if (prompt === "/exit" || prompt === "/quit") {
4733
6000
  break;
4734
6001
  }
6002
+ if (prompt === "/help") {
6003
+ process.stderr.write(`/provider [id] \xB7 /model [id] \xB7 /route [role=provider[:model]] \xB7 /status
6004
+ ` + `/diff \xB7 /review <task> \xB7 /resume <last|sessionId> \xB7 /compact
6005
+ ` + `/new [title] \xB7 /rename <title> \xB7 /clear \xB7 /exit
6006
+ `);
6007
+ continue;
6008
+ }
4735
6009
  if (prompt === "/clear") {
4736
6010
  messages = [];
4737
6011
  process.stderr.write(`Context cleared.
4738
6012
  `);
4739
6013
  continue;
4740
6014
  }
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))
6015
+ if (prompt === "/status") {
6016
+ process.stderr.write(`${await statusLine()}
6017
+ `);
6018
+ continue;
6019
+ }
6020
+ if (prompt === "/diff") {
6021
+ const diff = await harness.workspace.gitDiff();
6022
+ const output = `${diff.status.stdout}${diff.diff.stdout}${diff.staged.stdout}`;
6023
+ process.stdout.write(output || `No workspace changes.
6024
+ `);
6025
+ continue;
6026
+ }
6027
+ if (prompt === "/compact") {
6028
+ messages = compactHarnessMessages(messages);
6029
+ process.stderr.write(`Context compacted to ${messages.length} redacted message(s).
6030
+ `);
6031
+ continue;
6032
+ }
6033
+ if (prompt === "/provider" || prompt.startsWith("/provider ")) {
6034
+ const value = prompt.slice("/provider".length).trim();
6035
+ if (!value) {
6036
+ process.stderr.write(`${providerAvailability().map((provider2) => `${provider2.id}${provider2.id === harness.config.provider ? "*" : ""}`).join(" ")}
6037
+ `);
6038
+ continue;
6039
+ }
6040
+ const active2 = await hasActiveTurn();
6041
+ if (active2) {
6042
+ process.stderr.write(`Cannot switch provider while run ${active2.runId} is ${active2.status}.
6043
+ `);
6044
+ continue;
6045
+ }
6046
+ const provider = parseProvider(value);
6047
+ const model = DEFAULT_PROVIDER_REGISTRY.descriptor(provider).defaultModel;
6048
+ const portableMessages = compactHarnessMessages(messages);
6049
+ await replaceHarness({ ...runtimeOptions, provider, model }, routes);
6050
+ messages = portableMessages;
6051
+ process.stderr.write(`Next turn: ${provider}/${model}; context was compacted for a safe handoff.
6052
+ `);
6053
+ continue;
6054
+ }
6055
+ if (prompt === "/model" || prompt.startsWith("/model ")) {
6056
+ const value = prompt.slice("/model".length).trim();
6057
+ if (!value) {
6058
+ process.stderr.write(`${harness.config.provider}/${harness.config.model}
6059
+ `);
6060
+ continue;
6061
+ }
6062
+ const active2 = await hasActiveTurn();
6063
+ if (active2) {
6064
+ process.stderr.write(`Cannot switch model while run ${active2.runId} is ${active2.status}.
6065
+ `);
6066
+ continue;
6067
+ }
6068
+ const portableMessages = compactHarnessMessages(messages);
6069
+ await replaceHarness({ ...runtimeOptions, model: value }, routes);
6070
+ messages = portableMessages;
6071
+ process.stderr.write(`Next turn: ${harness.config.provider}/${value}; context was compacted.
6072
+ `);
6073
+ continue;
6074
+ }
6075
+ if (prompt === "/route" || prompt.startsWith("/route ")) {
6076
+ const value = prompt.slice("/route".length).trim();
6077
+ if (!value) {
6078
+ process.stderr.write(`${JSON.stringify(serializeHarnessModelRoutes(routes))}
6079
+ `);
6080
+ continue;
6081
+ }
6082
+ const active2 = await hasActiveTurn();
6083
+ if (active2) {
6084
+ process.stderr.write(`Cannot change routes while run ${active2.runId} is ${active2.status}.
6085
+ `);
6086
+ continue;
6087
+ }
6088
+ const nextRoutes = new Map(routes);
6089
+ if (value === "clear") {
6090
+ nextRoutes.clear();
6091
+ } else if (value.startsWith("clear ")) {
6092
+ const profile = value.slice("clear ".length).trim();
6093
+ if (!HARNESS_SUBAGENT_PROFILES.includes(profile)) {
6094
+ process.stderr.write(`Unknown subagent profile: ${profile}.
6095
+ `);
6096
+ continue;
6097
+ }
6098
+ nextRoutes.delete(profile);
6099
+ } else {
6100
+ const route = parseHarnessModelRoute(value);
6101
+ nextRoutes.set(route.profile, route);
6102
+ }
6103
+ await replaceHarness(runtimeOptions, nextRoutes);
6104
+ process.stderr.write(`Routes: ${JSON.stringify(serializeHarnessModelRoutes(routes))}
6105
+ `);
6106
+ continue;
6107
+ }
6108
+ if (prompt === "/new" || prompt.startsWith("/new ")) {
6109
+ const active2 = await hasActiveTurn();
6110
+ if (active2) {
6111
+ process.stderr.write(`Cannot leave session while run ${active2.runId} is ${active2.status}.
6112
+ `);
6113
+ continue;
6114
+ }
6115
+ const title = prompt.slice("/new".length).trim();
6116
+ const created = await sessionStore.create(title ? { title } : undefined);
6117
+ await restoreSession(created);
6118
+ process.stderr.write(`Created session ${session.sessionId}.
6119
+ `);
6120
+ continue;
6121
+ }
6122
+ if (prompt === "/rename" || prompt.startsWith("/rename ")) {
6123
+ const title = prompt.slice("/rename".length).trim();
6124
+ if (!title) {
6125
+ process.stderr.write(`Usage: /rename <title>
6126
+ `);
6127
+ continue;
6128
+ }
6129
+ session = await sessionStore.rename(session.sessionId, title);
6130
+ process.stderr.write(`Renamed session to ${session.title}.
6131
+ `);
6132
+ continue;
6133
+ }
6134
+ if (prompt === "/resume" || prompt.startsWith("/resume ")) {
6135
+ const selector = prompt.slice("/resume".length).trim() || "last";
6136
+ const selected = selector === "last" ? await sessionStore.latest({ includeArchived: true }) : await sessionStore.get(selector);
6137
+ if (!selected) {
6138
+ process.stderr.write(`Session ${selector} was not found.
6139
+ `);
6140
+ continue;
6141
+ }
6142
+ await restoreSession(selected);
6143
+ const active2 = await hasActiveTurn();
6144
+ process.stderr.write(active2 ? `Session ${session.sessionId} has run ${active2.runId} waiting in ${active2.status}; use zhx resume ${active2.runId} --approve|--deny.
6145
+ ` : `Resumed session ${session.sessionId}.
6146
+ `);
6147
+ continue;
6148
+ }
6149
+ if (prompt === "/review" || prompt.startsWith("/review ")) {
6150
+ const reviewPrompt = prompt.slice("/review".length).trim();
6151
+ if (!reviewPrompt) {
6152
+ process.stderr.write(`Usage: /review <task>
6153
+ `);
6154
+ continue;
6155
+ }
6156
+ const active2 = await hasActiveTurn();
6157
+ if (active2) {
6158
+ process.stderr.write(`Cannot start a review while run ${active2.runId} is ${active2.status}.
6159
+ `);
6160
+ continue;
6161
+ }
6162
+ const review = await withTemporaryProfiles(["explorer", "reviewer"], () => runHarnessReviewGroup(harness, { prompt: reviewPrompt, scope: harness.config.scope }, ["explorer", "reviewer"]));
6163
+ for (const output of review.outputs) {
6164
+ process.stdout.write(`
6165
+ [${output.name ?? output.agentId ?? "reviewer"}] ${output.status}
6166
+ `);
6167
+ if (output.output?.outputText)
6168
+ process.stdout.write(`${output.output.outputText}
6169
+ `);
6170
+ }
6171
+ continue;
6172
+ }
6173
+ const active = await hasActiveTurn();
6174
+ if (active) {
6175
+ process.stderr.write(`Run ${active.runId} is ${active.status}; resolve it with zhx resume ${active.runId} --approve|--deny before a new turn.
6176
+ `);
6177
+ continue;
6178
+ }
6179
+ const runId = `run_${randomUUID5()}`;
6180
+ session = await sessionStore.appendRun(session.sessionId, {
6181
+ runId,
6182
+ provider: harness.config.provider,
6183
+ model: harness.config.model,
6184
+ status: "created"
4753
6185
  });
6186
+ const turn = session.runs.at(-1);
6187
+ const tracker = { streamedText: false };
6188
+ let markedRunning = false;
6189
+ let result;
6190
+ try {
6191
+ result = await runHarness(harness, messages.length === 0 ? {
6192
+ runId,
6193
+ prompt,
6194
+ scope: harness.config.scope,
6195
+ metadata: {
6196
+ ...createHarnessResumeMetadata(harness.config, routes),
6197
+ zhivexCliSession: {
6198
+ schemaVersion: 1,
6199
+ sessionId: session.sessionId,
6200
+ turnId: turn.turnId,
6201
+ sequence: turn.sequence
6202
+ }
6203
+ }
6204
+ } : {
6205
+ runId,
6206
+ messages: appendUserMessage(messages, prompt),
6207
+ scope: harness.config.scope,
6208
+ metadata: {
6209
+ ...createHarnessResumeMetadata(harness.config, routes),
6210
+ zhivexCliSession: {
6211
+ schemaVersion: 1,
6212
+ sessionId: session.sessionId,
6213
+ turnId: turn.turnId,
6214
+ sequence: turn.sequence
6215
+ }
6216
+ }
6217
+ }, {
6218
+ onEvent: async (event) => {
6219
+ if (!markedRunning && event.type === "agent-run-start") {
6220
+ session = await sessionStore.updateRun(session.sessionId, runId, { status: "running" });
6221
+ markedRunning = true;
6222
+ }
6223
+ await streamSink({ json: false, jsonl: false }, tracker)(event);
6224
+ },
6225
+ resolveApprovals: terminalApprovalResolver(options.yes, (question) => readline.question(question))
6226
+ });
6227
+ } catch (error) {
6228
+ session = await sessionStore.updateRun(session.sessionId, runId, { status: "failed" });
6229
+ throw error;
6230
+ }
4754
6231
  if (!tracker.streamedText && result.outputText) {
4755
6232
  process.stdout.write(result.outputText);
4756
6233
  }
4757
6234
  process.stdout.write(`
4758
6235
  `);
4759
6236
  messages = result.messages;
6237
+ session = await sessionStore.updateRun(session.sessionId, runId, {
6238
+ status: sessionStatus(result.status)
6239
+ });
4760
6240
  if (result.status === "waiting_approval") {
4761
- process.stderr.write(`Run paused: ${result.state.runId}
6241
+ process.stderr.write(`Run paused: ${result.state.runId}. Resume with zhx resume ${result.state.runId} --approve|--deny.
4762
6242
  `);
4763
6243
  }
4764
6244
  }
4765
6245
  } finally {
4766
6246
  readline.close();
4767
6247
  harness.close();
6248
+ sessionStore.close();
4768
6249
  }
4769
6250
  };
4770
6251
  var providersDocument = (env = process.env) => ({
@@ -4819,7 +6300,7 @@ var satisfiesBunEngine = (version, range) => {
4819
6300
  var diagnostic = (id, status, message, details = {}) => ({ id, status, message, details });
4820
6301
  var inspectWorkspace = async (workspace) => {
4821
6302
  try {
4822
- const workspaceStat = await stat3(workspace);
6303
+ const workspaceStat = await stat4(workspace);
4823
6304
  if (!workspaceStat.isDirectory()) {
4824
6305
  return diagnostic("workspace", "fail", "Workspace is not a directory.", { path: workspace });
4825
6306
  }
@@ -4861,9 +6342,9 @@ var inspectGit = async (workspace) => {
4861
6342
  }
4862
6343
  };
4863
6344
  var inspectScripts = async (workspace, allowedChecks) => {
4864
- const packagePath = path8.join(workspace, "package.json");
6345
+ const packagePath = path9.join(workspace, "package.json");
4865
6346
  try {
4866
- const packageStat = await stat3(packagePath);
6347
+ const packageStat = await stat4(packagePath);
4867
6348
  if (!packageStat.isFile() || packageStat.size > 1024 * 1024) {
4868
6349
  return diagnostic("scripts", "warn", "package.json is not a readable regular file.", {
4869
6350
  packageJson: false,
@@ -4885,17 +6366,17 @@ var inspectScripts = async (workspace, allowedChecks) => {
4885
6366
  }
4886
6367
  };
4887
6368
  var isInsidePath2 = (root, candidate) => {
4888
- const relative = path8.relative(root, candidate);
4889
- return relative === "" || !relative.startsWith(`..${path8.sep}`) && relative !== ".." && !path8.isAbsolute(relative);
6369
+ const relative = path9.relative(root, candidate);
6370
+ return relative === "" || !relative.startsWith(`..${path9.sep}`) && relative !== ".." && !path9.isAbsolute(relative);
4890
6371
  };
4891
6372
  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] ?? "");
6373
+ const parsed = path9.parse(candidate);
6374
+ const parts = trustedPrefix ? path9.relative(trustedPrefix, candidate).split(path9.sep).filter(Boolean) : candidate.slice(parsed.root.length).split(path9.sep).filter(Boolean).slice(1);
6375
+ let current = trustedPrefix ?? path9.join(parsed.root, candidate.slice(parsed.root.length).split(path9.sep).filter(Boolean)[0] ?? "");
4895
6376
  for (const part of parts) {
4896
- current = path8.join(current, part);
6377
+ current = path9.join(current, part);
4897
6378
  try {
4898
- if ((await lstat6(current)).isSymbolicLink()) {
6379
+ if ((await lstat7(current)).isSymbolicLink()) {
4899
6380
  return true;
4900
6381
  }
4901
6382
  } catch (error) {
@@ -4911,12 +6392,12 @@ var nearestExistingPath = async (candidate) => {
4911
6392
  let current = candidate;
4912
6393
  for (;; ) {
4913
6394
  try {
4914
- return { path: current, stat: await stat3(current) };
6395
+ return { path: current, stat: await stat4(current) };
4915
6396
  } catch (error) {
4916
6397
  if (error.code !== "ENOENT") {
4917
6398
  throw error;
4918
6399
  }
4919
- const parent = path8.dirname(current);
6400
+ const parent = path9.dirname(current);
4920
6401
  if (parent === current) {
4921
6402
  throw error;
4922
6403
  }
@@ -4932,7 +6413,7 @@ var inspectStateDirectory = async (workspace, stateDirectory) => {
4932
6413
  });
4933
6414
  }
4934
6415
  if (isInsidePath2(workspace, stateDirectory)) {
4935
- const segments = path8.relative(workspace, stateDirectory).split(path8.sep).filter(Boolean);
6416
+ const segments = path9.relative(workspace, stateDirectory).split(path9.sep).filter(Boolean);
4936
6417
  const sensitiveSegment = segments.find(isSensitiveStateSegment);
4937
6418
  if (sensitiveSegment) {
4938
6419
  return diagnostic("state-directory", "fail", "State directory is inside a sensitive workspace path.", {
@@ -4979,9 +6460,9 @@ var inspectOperationsStore = async (stateDirectory, storeBackend) => {
4979
6460
  migrationAvailable: true
4980
6461
  });
4981
6462
  }
4982
- const databasePath = path8.join(stateDirectory, "operations.sqlite");
6463
+ const databasePath = path9.join(stateDirectory, "operations.sqlite");
4983
6464
  try {
4984
- const entry = await lstat6(databasePath);
6465
+ const entry = await lstat7(databasePath);
4985
6466
  if (entry.isSymbolicLink() || !entry.isFile()) {
4986
6467
  return diagnostic("operations-store", "fail", "SQLite state path is not a regular file.", {
4987
6468
  backend: storeBackend,
@@ -5269,6 +6750,95 @@ var manageRuns = async (options) => {
5269
6750
  persistence.close();
5270
6751
  }
5271
6752
  };
6753
+ var openSessionStoreForConfig = (config) => openCliSessionStore({
6754
+ workspace: config.workspace,
6755
+ stateDirectory: config.stateDirectory,
6756
+ scope: config.scope
6757
+ });
6758
+ var printSessionDocument = (document, json) => {
6759
+ if (json) {
6760
+ process.stdout.write(`${JSON.stringify(document, null, 2)}
6761
+ `);
6762
+ return;
6763
+ }
6764
+ const record = document;
6765
+ if (record.kind === "session-list") {
6766
+ for (const session2 of record.sessions ?? []) {
6767
+ const latest = session2.runs.at(-1);
6768
+ process.stdout.write(`${session2.sessionId} ${session2.title ?? "(untitled)"} \xB7 ${session2.runs.length} runs` + `${latest ? ` \xB7 ${latest.provider}/${latest.model} \xB7 ${latest.status}` : ""}
6769
+ `);
6770
+ }
6771
+ return;
6772
+ }
6773
+ const session = record.session;
6774
+ if (session) {
6775
+ process.stdout.write(`${session.sessionId} \xB7 ${session.title ?? "(untitled)"} \xB7 ${session.runs.length} runs` + `${session.archivedAt ? " \xB7 archived" : ""}
6776
+ `);
6777
+ return;
6778
+ }
6779
+ process.stdout.write(`${JSON.stringify(document, null, 2)}
6780
+ `);
6781
+ };
6782
+ var manageSessions = async (options) => {
6783
+ const config = resolveHarnessConfig(options);
6784
+ const store = await openSessionStoreForConfig(config);
6785
+ try {
6786
+ let session;
6787
+ let document;
6788
+ switch (options.sessionsCommand) {
6789
+ case "list": {
6790
+ const summaries = await store.list(options.limit ? { limit: options.limit } : undefined);
6791
+ const sessions = (await Promise.all(summaries.map((summary) => store.get(summary.sessionId)))).filter((value) => Boolean(value));
6792
+ document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session-list", sessions };
6793
+ break;
6794
+ }
6795
+ case "inspect":
6796
+ session = await store.get(options.sessionId);
6797
+ if (!session)
6798
+ throw new Error(`Session ${options.sessionId} was not found.`);
6799
+ document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
6800
+ break;
6801
+ case "rename":
6802
+ session = await store.rename(options.sessionId, options.sessionTitle);
6803
+ document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
6804
+ break;
6805
+ case "fork":
6806
+ session = await store.fork(options.sessionId);
6807
+ document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
6808
+ break;
6809
+ case "archive":
6810
+ session = await store.archive(options.sessionId);
6811
+ document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
6812
+ break;
6813
+ default:
6814
+ throw new CliUsageError("sessions requires one of: list, inspect, rename, fork, archive.");
6815
+ }
6816
+ printSessionDocument(document, options.json);
6817
+ } finally {
6818
+ store.close();
6819
+ }
6820
+ };
6821
+ var sessionStatus = (status) => {
6822
+ if (status === "queued")
6823
+ return "created";
6824
+ if (status === "suspended")
6825
+ return "waiting_approval";
6826
+ return status;
6827
+ };
6828
+ var updateIndexedSessionRun = async (config, runId, status) => {
6829
+ const store = await openSessionStoreForConfig(config);
6830
+ try {
6831
+ for (const summary of await store.list({ limit: 200, includeArchived: true })) {
6832
+ const session = await store.get(summary.sessionId);
6833
+ if (session?.runs.some((run) => run.runId === runId)) {
6834
+ await store.updateRun(session.sessionId, runId, { status: sessionStatus(status) });
6835
+ return;
6836
+ }
6837
+ }
6838
+ } finally {
6839
+ store.close();
6840
+ }
6841
+ };
5272
6842
  var cliExitCodeForError = (error) => error instanceof CliUsageError ? CLI_EXIT_CODES.usageError : CLI_EXIT_CODES.runtimeError;
5273
6843
  var main = async (argv = process.argv.slice(2)) => {
5274
6844
  const options = parseCliArgs(argv);
@@ -5296,7 +6866,14 @@ var main = async (argv = process.argv.slice(2)) => {
5296
6866
  case "runs":
5297
6867
  await manageRuns(options);
5298
6868
  return;
6869
+ case "sessions":
6870
+ await manageSessions(options);
6871
+ return;
5299
6872
  case "run":
6873
+ if (options.implicitCommand && !options.prompt && process.stdin.isTTY && process.stdout.isTTY) {
6874
+ await chat(options);
6875
+ return;
6876
+ }
5300
6877
  await runOnce(options);
5301
6878
  return;
5302
6879
  case "review":
@@ -5312,9 +6889,11 @@ if (import.meta.main) {
5312
6889
  });
5313
6890
  }
5314
6891
  export {
6892
+ withTemporaryHarnessProfiles,
5315
6893
  summarizeApproval,
5316
6894
  runResultDocument,
5317
6895
  resumeCommand,
6896
+ readHarnessResumeRoutes,
5318
6897
  readHarnessResumeConfig,
5319
6898
  providersDocument,
5320
6899
  parseCliArgs,