@hilbras/omninode 2.0.0-alpha.2 → 2.0.0-alpha.20

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/index.js CHANGED
@@ -1,12 +1,3 @@
1
- // src/types/task.ts
2
- var TERMINAL_TASK_STATUSES = [
3
- "completed",
4
- "failed",
5
- "timed_out",
6
- "cancelled",
7
- "partially_completed"
8
- ];
9
-
10
1
  // src/errors/index.ts
11
2
  var OmniNodeError = class extends Error {
12
3
  code;
@@ -80,16 +71,19 @@ var ConsoleLogSink = class {
80
71
  };
81
72
  var Logger = class _Logger {
82
73
  level;
74
+ format;
83
75
  sink;
84
76
  context;
85
77
  constructor(options = {}) {
86
78
  this.level = options.level ?? "info";
79
+ this.format = options.format ?? "text";
87
80
  this.sink = options.sink ?? new ConsoleLogSink();
88
81
  this.context = options.context ?? {};
89
82
  }
90
83
  child(context) {
91
84
  return new _Logger({
92
85
  level: this.level,
86
+ format: this.format,
93
87
  sink: this.sink,
94
88
  context: { ...this.context, ...context }
95
89
  });
@@ -108,8 +102,13 @@ var Logger = class _Logger {
108
102
  }
109
103
  log(level, message, data) {
110
104
  if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) return;
111
- const parts = [(/* @__PURE__ */ new Date()).toISOString(), level.toUpperCase(), message];
105
+ const at = (/* @__PURE__ */ new Date()).toISOString();
112
106
  const merged = { ...this.context, ...data };
107
+ if (this.format === "json") {
108
+ this.sink.write(level, JSON.stringify({ at, level, message, ...merged }));
109
+ return;
110
+ }
111
+ const parts = [at, level.toUpperCase(), message];
113
112
  if (Object.keys(merged).length > 0) {
114
113
  parts.push(JSON.stringify(merged));
115
114
  }
@@ -117,6 +116,10 @@ var Logger = class _Logger {
117
116
  }
118
117
  };
119
118
  var logger = new Logger();
119
+ function configureLogger(options) {
120
+ if (options.level !== void 0) logger.level = options.level;
121
+ if (options.format !== void 0) logger.format = options.format;
122
+ }
120
123
 
121
124
  // src/config/schema.ts
122
125
  import { z } from "zod";
@@ -125,6 +128,7 @@ var providerConfigSchema = z.object({
125
128
  type: z.enum(["openai-compatible", "omnihilbras", "openrouter", "local", "custom"]),
126
129
  base_url: z.string().url(),
127
130
  api_key_env_var: z.string().min(1).optional(),
131
+ timeout_ms: z.number().int().positive().optional(),
128
132
  headers: z.record(z.string()).optional(),
129
133
  enabled: z.boolean().optional(),
130
134
  metadata: z.record(z.unknown()).optional()
@@ -140,6 +144,11 @@ var agentConfigSchema = z.object({
140
144
  input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
141
145
  timeout_ms: z.number().int().positive().optional(),
142
146
  inherit_env: z.boolean().optional(),
147
+ env_policy: z.enum(["inherit", "allowlist", "denylist", "explicit"]).optional(),
148
+ env_allowlist: z.array(z.string()).optional(),
149
+ env_denylist: z.array(z.string()).optional(),
150
+ max_output_bytes: z.number().int().positive().optional(),
151
+ allow_external_cwd: z.boolean().optional(),
143
152
  metadata: z.record(z.unknown()).optional()
144
153
  }).strict();
145
154
  var roleConfigSchema = z.object({
@@ -160,7 +169,9 @@ var pipelineStepConfigSchema = z.object({
160
169
  model: z.string().optional(),
161
170
  depends_on: z.array(z.string()).optional(),
162
171
  retries: z.number().int().min(0).max(10).optional(),
163
- condition: z.enum(["always", "on-success", "on-failure"]).optional()
172
+ condition: z.enum(["always", "on-success", "on-failure"]).optional(),
173
+ /** Constraints passed to the planner for plan steps (§15). */
174
+ constraints: z.array(z.string()).optional()
164
175
  }).strict();
165
176
  var pipelineConfigSchema = z.object({
166
177
  id: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "pipeline id must be kebab-case"),
@@ -184,14 +195,135 @@ var projectConfigSchema = z.object({
184
195
  memory: z.object({
185
196
  provider: z.string().min(1).default("local"),
186
197
  base_url: z.string().url().optional(),
187
- api_key_env_var: z.string().min(1).optional()
198
+ api_key_env_var: z.string().min(1).optional(),
199
+ /** When true, memory failures fail tasks instead of being tolerated (§12). */
200
+ required: z.boolean().optional()
188
201
  }).strict().optional()
189
202
  }).strict();
190
203
  var appConfigSchema = z.object({
204
+ /** Active profile; overlays live under `profiles` (§16). */
205
+ profile: z.string().min(1).optional(),
206
+ /** Named partial overlays, e.g. development/production/testing. */
207
+ profiles: z.record(z.unknown()).optional(),
191
208
  project: projectConfigSchema,
192
- logging: z.object({ level: z.enum(["debug", "info", "warn", "error"]) }).strict().optional()
209
+ logging: z.object({
210
+ level: z.enum(["debug", "info", "warn", "error"]),
211
+ format: z.enum(["text", "json"]).optional()
212
+ }).strict().optional()
193
213
  }).strict();
194
214
 
215
+ // src/config/secrets.ts
216
+ var SAFE_KEY_SUFFIXES = ["_env", "_env_var", "_file", "_path", "_ref", "_id", "_url", "_command"];
217
+ var SECRET_KEY = /^\s*([A-Za-z0-9_-]*(?:api[_-]?key|token|password|passwd|secret|credential)[A-Za-z0-9_-]*)\s*:\s*(.*)$/i;
218
+ var SAFE_VALUE = /^\s*(?:\$\{[^}]*\}|\$[A-Z_][A-Z0-9_]*|<[^>]*>|""|''|null|~)\s*$/i;
219
+ function scanForInlineSecrets(source) {
220
+ const findings = [];
221
+ source.split("\n").forEach((line, index) => {
222
+ const trimmed = line.trim();
223
+ if (trimmed.startsWith("#")) return;
224
+ const match = SECRET_KEY.exec(line);
225
+ if (!match) return;
226
+ const key = match[1] ?? "";
227
+ const rawValue = (match[2] ?? "").trim().replace(/,\s*$/, "");
228
+ const keyIsReference = SAFE_KEY_SUFFIXES.some((suffix) => key.toLowerCase().endsWith(suffix));
229
+ if (keyIsReference) return;
230
+ const value = rawValue.replace(/^["']|["']$/g, "");
231
+ if (value.length === 0 || SAFE_VALUE.test(rawValue)) return;
232
+ if (value.length < 8) return;
233
+ findings.push({
234
+ line: index + 1,
235
+ key,
236
+ excerpt: `${key}: ${value.slice(0, 3)}\u2026 (${value.length} chars, redacted)`
237
+ });
238
+ });
239
+ return findings;
240
+ }
241
+ function describeSecretFindings(file, findings) {
242
+ const lines = findings.map((f) => ` line ${f.line}: ${f.excerpt}`).join("\n");
243
+ return `${file} appears to contain ${findings.length} inline secret(s):
244
+ ${lines}
245
+ OmniNode never stores credentials in configuration. Replace each value with an environment-variable reference, e.g. api_key_env_var: MY_API_KEY.`;
246
+ }
247
+
248
+ // src/config/profiles.ts
249
+ import { homedir } from "os";
250
+ import path from "path";
251
+ function userConfigPath(env = process.env) {
252
+ if (env.OMNINODE_USER_CONFIG) return env.OMNINODE_USER_CONFIG;
253
+ const home = env.HOME ?? homedir();
254
+ return path.join(home, ".omninode", "config.yaml");
255
+ }
256
+ function resolveProfile(input, env) {
257
+ if (env.OMNINODE_PROFILE) return env.OMNINODE_PROFILE;
258
+ if (typeof input.profile === "string" && input.profile.length > 0) return input.profile;
259
+ return "default";
260
+ }
261
+ function mergeConfig(base, overlay) {
262
+ if (!isPlainObject(base) || !isPlainObject(overlay)) {
263
+ return overlay === void 0 ? base : overlay;
264
+ }
265
+ const result = { ...base };
266
+ for (const [key, value] of Object.entries(overlay)) {
267
+ const existing = result[key];
268
+ if (Array.isArray(value)) {
269
+ result[key] = value;
270
+ } else if (isPlainObject(value) && isPlainObject(existing)) {
271
+ result[key] = mergeConfig(existing, value);
272
+ } else {
273
+ result[key] = value;
274
+ }
275
+ }
276
+ return result;
277
+ }
278
+ function isPlainObject(value) {
279
+ return value !== null && typeof value === "object" && !Array.isArray(value);
280
+ }
281
+ function asObject(value) {
282
+ return isPlainObject(value) ? value : {};
283
+ }
284
+ function applyEnvOverrides(input, env) {
285
+ const applied = [];
286
+ const config = mergeConfig(input, {});
287
+ if (env.OMNINODE_LOG_LEVEL) {
288
+ config.logging = { ...asObject(config.logging), level: env.OMNINODE_LOG_LEVEL };
289
+ applied.push("OMNINODE_LOG_LEVEL");
290
+ }
291
+ if (env.OMNINODE_LOG_FORMAT) {
292
+ config.logging = { ...asObject(config.logging), format: env.OMNINODE_LOG_FORMAT };
293
+ applied.push("OMNINODE_LOG_FORMAT");
294
+ }
295
+ const memoryProvider = env.OMNINODE_MEMORY_PROVIDER;
296
+ const memoryRequired = env.OMNINODE_MEMORY_REQUIRED;
297
+ if (memoryProvider || memoryRequired) {
298
+ const project = asObject(config.project);
299
+ config.project = {
300
+ ...project,
301
+ memory: {
302
+ ...asObject(project.memory ?? { provider: "local" }),
303
+ ...memoryProvider ? { provider: memoryProvider } : {},
304
+ ...memoryRequired ? { required: memoryRequired !== "false" } : {}
305
+ }
306
+ };
307
+ if (memoryProvider) applied.push("OMNINODE_MEMORY_PROVIDER");
308
+ if (memoryRequired) applied.push("OMNINODE_MEMORY_REQUIRED");
309
+ }
310
+ return { config, applied };
311
+ }
312
+ function applyDefaults(input) {
313
+ const applied = [];
314
+ const config = mergeConfig(input, {});
315
+ const logging = asObject(config.logging);
316
+ if (logging.level === void 0) {
317
+ config.logging = { ...logging, level: "info" };
318
+ applied.push("logging.level");
319
+ }
320
+ if (logging.format === void 0) {
321
+ config.logging = { ...asObject(config.logging), format: "text" };
322
+ applied.push("logging.format");
323
+ }
324
+ return { config, applied };
325
+ }
326
+
195
327
  // src/config/loader.ts
196
328
  import { existsSync, readFileSync } from "fs";
197
329
  import { parse as parseYaml } from "yaml";
@@ -218,34 +350,132 @@ function expandEnvRefs(text, env, source = "config") {
218
350
  }
219
351
  );
220
352
  }
221
- function loadConfig(options = {}) {
353
+ function loadConfigDetailed(options = {}) {
222
354
  const env = options.env ?? process.env;
223
- const path = options.path ?? (options.directory ? findConfigFile(options.directory) : findConfigFile());
224
- if (!path) {
355
+ const explicitPath = options.path;
356
+ let input = {};
357
+ let projectFile;
358
+ let userFile;
359
+ if (!explicitPath) {
360
+ const candidate = userConfigPath(env);
361
+ if (existsSync(candidate)) {
362
+ input = mergeConfig(input, readConfigDocument(candidate, env));
363
+ userFile = candidate;
364
+ }
365
+ }
366
+ const resolvedProject = explicitPath ?? (options.directory ? findConfigFile(options.directory) : findConfigFile());
367
+ if (resolvedProject) {
368
+ input = mergeConfig(input, readConfigDocument(resolvedProject, env));
369
+ projectFile = resolvedProject;
370
+ }
371
+ if (!projectFile && !userFile) {
225
372
  throw new ConfigError(
226
373
  "CONFIG_NOT_FOUND",
227
374
  "No omninode.yaml found. Run `omninode init` to create one, or pass an explicit path."
228
375
  );
229
376
  }
230
- const raw = readFileSync(path, "utf8");
231
- const expanded = expandEnvRefs(raw, env, path);
232
- let data;
233
- try {
234
- data = parseYaml(expanded);
235
- } catch (error) {
236
- throw new ConfigError("CONFIG_INVALID", `Failed to parse ${path}.`, { cause: error });
377
+ const profile = options.profile ?? resolveProfile(input, env);
378
+ const overlay = input.profiles?.[profile];
379
+ if (overlay !== void 0) {
380
+ const { profiles: _drop, ...base } = input;
381
+ input = mergeConfig(base, overlay);
382
+ } else if (profile !== "default" && profile !== "") {
383
+ throw new ConfigError(
384
+ "CONFIG_INVALID",
385
+ `Profile "${profile}" is not defined in this configuration (known: ${Object.keys(input.profiles ?? {}).join(", ") || "none"}).`
386
+ );
237
387
  }
238
- const parsed = appConfigSchema.safeParse(data);
388
+ const withEnv = applyEnvOverrides(input, env);
389
+ const withDefaults = applyDefaults(withEnv.config);
390
+ const parsed = appConfigSchema.safeParse(withDefaults.config);
239
391
  if (!parsed.success) {
240
392
  const issues = parsed.error.issues.map((issue) => {
241
- const location = issue.path.length > 0 ? issue.path.join(".") : "(root)";
393
+ const location = issue.path.join(".") || "(root)";
242
394
  return `${location}: ${issue.message}`;
243
395
  });
244
- throw new ConfigError("CONFIG_INVALID", `Invalid configuration in ${path}.`, {
245
- details: { issues }
246
- });
396
+ throw new ConfigError(
397
+ "CONFIG_INVALID",
398
+ `Invalid configuration:
399
+ ${issues.join("\n ")}`,
400
+ { details: { issues } }
401
+ );
247
402
  }
248
- return toAppConfig(parsed.data);
403
+ assertUniqueNames(parsed.data);
404
+ return {
405
+ config: toAppConfig(parsed.data),
406
+ sources: {
407
+ ...projectFile !== void 0 ? { projectFile } : {},
408
+ ...userFile !== void 0 ? { userFile } : {},
409
+ profile,
410
+ envOverrides: withEnv.applied,
411
+ defaults: withDefaults.applied
412
+ }
413
+ };
414
+ }
415
+ function assertUniqueNames(data) {
416
+ const groups = [
417
+ ["provider", data.project.providers.map((p) => p.name)],
418
+ ["agent", data.project.agents.map((a) => a.name)],
419
+ ["role", data.project.roles.map((r) => r.id)],
420
+ ["pipeline", data.project.pipelines.map((p) => p.id)]
421
+ ];
422
+ for (const [kind, names] of groups) {
423
+ const seen = /* @__PURE__ */ new Set();
424
+ for (const name of names) {
425
+ if (seen.has(name)) {
426
+ throw new ConfigError(
427
+ "CONFIG_INVALID",
428
+ `Duplicate ${kind} name "${name}" \u2014 every ${kind} must have a unique name.`
429
+ );
430
+ }
431
+ seen.add(name);
432
+ }
433
+ }
434
+ }
435
+ function readConfigDocument(path2, optionsEnv) {
436
+ const raw = readFileSync(path2, "utf8");
437
+ const secretFindings = scanForInlineSecrets(raw);
438
+ if (secretFindings.length > 0) {
439
+ throw new ConfigError("CONFIG_INVALID", describeSecretFindings(path2, secretFindings));
440
+ }
441
+ const expanded = expandEnvRefs(raw, optionsEnv, path2);
442
+ try {
443
+ return parseYaml(expanded);
444
+ } catch (error) {
445
+ throw new ConfigError("CONFIG_INVALID", `Failed to parse ${path2}.`, { cause: error });
446
+ }
447
+ }
448
+ function loadConfig(options = {}) {
449
+ return loadConfigDetailed(options).config;
450
+ }
451
+ function configDiagnostics(config) {
452
+ const notes = [];
453
+ for (const provider of config.project.providers) {
454
+ if (!provider.apiKeyEnvVar && provider.baseUrl.startsWith("https://")) {
455
+ notes.push(
456
+ `Provider "${provider.name}": API key reference is missing \u2014 add api_key_env_var: <ENV_VAR> unless this endpoint is intentionally keyless.`
457
+ );
458
+ }
459
+ }
460
+ for (const agent of config.project.agents) {
461
+ if (!agent.command && agent.integration !== "custom") {
462
+ notes.push(`Agent "${agent.name}": no command configured \u2014 the process adapter needs one.`);
463
+ }
464
+ if (agent.allowExternalCwd) {
465
+ notes.push(`Agent "${agent.name}": may run outside the project root (allow_external_cwd).`);
466
+ }
467
+ }
468
+ for (const pipeline of config.project.pipelines) {
469
+ const ids = new Set(pipeline.steps.map((s) => s.id));
470
+ for (const step of pipeline.steps) {
471
+ for (const dep of step.dependsOn ?? []) {
472
+ if (!ids.has(dep)) {
473
+ notes.push(`Pipeline "${pipeline.id}": step "${step.id}" depends on unknown step "${dep}".`);
474
+ }
475
+ }
476
+ }
477
+ }
478
+ return notes;
249
479
  }
250
480
  function toAppConfig(yaml) {
251
481
  return {
@@ -266,7 +496,8 @@ function toAppConfig(yaml) {
266
496
  memory: {
267
497
  provider: yaml.project.memory.provider,
268
498
  baseUrl: yaml.project.memory.base_url,
269
- apiKeyEnvVar: yaml.project.memory.api_key_env_var
499
+ apiKeyEnvVar: yaml.project.memory.api_key_env_var,
500
+ required: yaml.project.memory.required
270
501
  }
271
502
  } : {}
272
503
  },
@@ -287,7 +518,8 @@ function toPipelineDefinition(raw) {
287
518
  model: step.model,
288
519
  dependsOn: step.depends_on,
289
520
  retries: step.retries,
290
- condition: step.condition
521
+ condition: step.condition,
522
+ constraints: step.constraints
291
523
  }))
292
524
  };
293
525
  }
@@ -297,6 +529,7 @@ function toProviderConfig(raw) {
297
529
  type: raw.type,
298
530
  baseUrl: raw.base_url,
299
531
  apiKeyEnvVar: raw.api_key_env_var,
532
+ timeoutMs: raw.timeout_ms,
300
533
  headers: raw.headers,
301
534
  enabled: raw.enabled,
302
535
  metadata: raw.metadata
@@ -314,6 +547,11 @@ function toAgentConfig(raw) {
314
547
  inputMode: raw.input_mode,
315
548
  timeoutMs: raw.timeout_ms,
316
549
  inheritEnv: raw.inherit_env,
550
+ envPolicy: raw.env_policy,
551
+ envAllowlist: raw.env_allowlist,
552
+ envDenylist: raw.env_denylist,
553
+ maxOutputBytes: raw.max_output_bytes,
554
+ allowExternalCwd: raw.allow_external_cwd,
317
555
  metadata: raw.metadata
318
556
  };
319
557
  }
@@ -384,57 +622,213 @@ function authHeaders(config, env = process.env) {
384
622
  }
385
623
 
386
624
  // src/providers/http.ts
625
+ var HttpResponseError = class extends Error {
626
+ constructor(message, status, cause) {
627
+ super(message, { cause });
628
+ this.status = status;
629
+ this.name = "HttpResponseError";
630
+ }
631
+ status;
632
+ };
387
633
  var DEFAULT_TIMEOUT_MS = 3e4;
388
634
  async function requestJson(url, options = {}) {
389
635
  const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
390
- let response;
391
- try {
392
- response = await fetch(url, {
393
- method,
394
- headers: {
395
- accept: "application/json",
396
- ...body !== void 0 ? { "content-type": "application/json" } : {},
397
- ...headers
398
- },
399
- ...body !== void 0 ? { body: JSON.stringify(body) } : {},
400
- signal: AbortSignal.timeout(timeoutMs)
401
- });
402
- } catch (error) {
403
- const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
404
- throw new ProviderError("PROVIDER_UNAVAILABLE", `Request to ${url} failed: ${reason}.`, {
405
- cause: error
406
- });
407
- }
636
+ const response = await fetch(url, {
637
+ method,
638
+ headers: {
639
+ accept: "application/json",
640
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
641
+ ...headers
642
+ },
643
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
644
+ signal: AbortSignal.timeout(timeoutMs)
645
+ });
408
646
  let parsed;
409
647
  try {
410
648
  parsed = await response.json();
411
649
  } catch (error) {
412
- throw new ProviderError("PROVIDER_UNAVAILABLE", `Response from ${url} was not valid JSON.`, {
413
- cause: error,
414
- details: { status: response.status }
415
- });
650
+ throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
416
651
  }
417
652
  return { status: response.status, body: parsed };
418
653
  }
419
654
 
655
+ // src/providers/errors.ts
656
+ function kindToCode(kind) {
657
+ switch (kind) {
658
+ case "AUTHENTICATION_ERROR":
659
+ return "PROVIDER_AUTH_FAILED";
660
+ case "MODEL_NOT_FOUND":
661
+ return "MODEL_NOT_FOUND";
662
+ default:
663
+ return "PROVIDER_UNAVAILABLE";
664
+ }
665
+ }
666
+ function isRetryable(kind) {
667
+ return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
668
+ }
669
+ function kindFromStatus(status) {
670
+ if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
671
+ if (status === 429) return "RATE_LIMIT_ERROR";
672
+ if (status === 404) return "MODEL_NOT_FOUND";
673
+ if (status === 408 || status === 504) return "TIMEOUT";
674
+ if (status >= 500) return "SERVER_ERROR";
675
+ if (status >= 400) return "INVALID_REQUEST";
676
+ return "UNKNOWN_ERROR";
677
+ }
678
+ function extractMessage(body) {
679
+ if (body === null || typeof body !== "object") return void 0;
680
+ const root = body;
681
+ const error = root.error ?? root;
682
+ const message = error.message ?? error.detail ?? root.message;
683
+ return typeof message === "string" ? message : void 0;
684
+ }
685
+ function parseRetryAfter(value, now = Date.now()) {
686
+ if (!value) return void 0;
687
+ const seconds = Number(value);
688
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
689
+ const date = Date.parse(value);
690
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
691
+ }
692
+ function isTimeoutError(error) {
693
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
694
+ }
695
+ function isNetworkError(error) {
696
+ return error instanceof TypeError || typeof error?.cause === "object" && error instanceof Error && !error.code;
697
+ }
698
+ function fallbackMessage(kind, context, status) {
699
+ const { provider, operation } = context;
700
+ switch (kind) {
701
+ case "AUTHENTICATION_ERROR":
702
+ return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
703
+ case "RATE_LIMIT_ERROR":
704
+ return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
705
+ case "MODEL_NOT_FOUND":
706
+ return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
707
+ case "TIMEOUT":
708
+ return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
709
+ case "SERVER_ERROR":
710
+ return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
711
+ case "INVALID_REQUEST":
712
+ return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
713
+ default:
714
+ return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
715
+ }
716
+ }
717
+ function providerHttpError(context) {
718
+ const status = context.status ?? 0;
719
+ const kind = kindFromStatus(status);
720
+ const detail = extractMessage(context.body);
721
+ const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
722
+ return new ProviderError(
723
+ context.code ?? kindToCode(kind),
724
+ detail ?? fallbackMessage(kind, context, status),
725
+ {
726
+ cause: context.cause,
727
+ details: {
728
+ kind,
729
+ provider: context.provider,
730
+ operation: context.operation,
731
+ status,
732
+ retryable: isRetryable(kind),
733
+ ...retryAfterMs !== void 0 ? { retryAfterMs } : {},
734
+ ...detail ? { providerMessage: detail } : {}
735
+ }
736
+ }
737
+ );
738
+ }
739
+ function providerTransportError(context) {
740
+ const cause = context.cause;
741
+ const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
742
+ const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
743
+ return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
744
+ cause: context.cause,
745
+ details: {
746
+ kind,
747
+ provider: context.provider,
748
+ operation: context.operation,
749
+ retryable: isRetryable(kind)
750
+ }
751
+ });
752
+ }
753
+
420
754
  // src/providers/openai-compatible/index.ts
755
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
421
756
  var OpenAICompatibleProvider = class {
422
757
  config;
758
+ providerId;
759
+ authentication;
423
760
  log;
424
- constructor(config, log = logger) {
761
+ /** Discovered models, so getModel() answers without another round-trip. */
762
+ discovered;
763
+ env;
764
+ audit;
765
+ constructor(config, log = logger, env = process.env, audit) {
766
+ this.audit = audit;
425
767
  this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
768
+ this.env = env;
769
+ this.providerId = config.providerId ?? config.name;
770
+ this.authentication = {
771
+ method: config.apiKeyEnvVar ? "bearer" : "none",
772
+ ...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
773
+ configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
774
+ };
426
775
  this.log = log.child({ provider: config.name });
427
776
  }
428
- async listModels() {
429
- const { status, body } = await requestJson(`${this.config.baseUrl}/models`, {
430
- headers: authHeaders(this.config)
431
- });
432
- if (status === 401 || status === 403) {
433
- throw authFailed(this.config, status);
777
+ /** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
778
+ get requestTimeoutMs() {
779
+ return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
780
+ }
781
+ /** Authenticate + discover (roadmap §10, Model Discovery). */
782
+ async connect() {
783
+ authHeaders(this.config, this.env);
784
+ const models = await this.listModels();
785
+ this.discovered = models;
786
+ return models;
787
+ }
788
+ async getModel(modelId) {
789
+ const models = this.discovered ?? await this.connect();
790
+ return models.find((m) => m.id === modelId);
791
+ }
792
+ /** Raw /models body (protected so adapters can capture gateway-level metadata). */
793
+ async rawModelsResponse() {
794
+ const response = await this.modelsRequest();
795
+ return response.body;
796
+ }
797
+ async modelsRequest() {
798
+ let response;
799
+ try {
800
+ response = await requestJson(`${this.config.baseUrl}/models`, {
801
+ headers: authHeaders(this.config, this.env),
802
+ timeoutMs: this.requestTimeoutMs
803
+ });
804
+ } catch (error) {
805
+ if (error instanceof ProviderError) {
806
+ await this.recordProviderError("listModels", error);
807
+ throw error;
808
+ }
809
+ const normalized = providerTransportError({
810
+ provider: this.config.name,
811
+ operation: "listModels",
812
+ cause: error
813
+ });
814
+ await this.recordProviderError("listModels", normalized);
815
+ throw normalized;
434
816
  }
817
+ const { status, body } = response;
435
818
  if (status !== 200) {
436
- throw unavailable(this.config, `Model discovery returned HTTP ${status}.`, status);
819
+ const httpError = providerHttpError({
820
+ provider: this.config.name,
821
+ operation: "listModels",
822
+ status,
823
+ body
824
+ });
825
+ await this.recordProviderError("listModels", httpError);
826
+ throw httpError;
437
827
  }
828
+ return response;
829
+ }
830
+ async listModels() {
831
+ const body = await this.rawModelsResponse();
438
832
  const entries = extractModelEntries(body, this.config);
439
833
  const models = [];
440
834
  let skipped = 0;
@@ -443,22 +837,59 @@ var OpenAICompatibleProvider = class {
443
837
  skipped += 1;
444
838
  continue;
445
839
  }
446
- models.push({
447
- provider: this.config.name,
448
- id: entry.id,
449
- displayName: entry.id,
450
- status: "available",
451
- metadata: {
452
- ...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
453
- ...typeof entry.created === "number" ? { createdAt: entry.created } : {}
454
- }
455
- });
840
+ models.push(this.toModelInfo(entry));
456
841
  }
457
842
  if (skipped > 0) {
458
843
  this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
459
844
  }
460
845
  return models;
461
846
  }
847
+ /** Maps a raw model entry onto the normalized metadata shape (§10). */
848
+ toModelInfo(entry) {
849
+ const parameters = Array.isArray(entry.supported_parameters) ? entry.supported_parameters.filter((p) => typeof p === "string") : void 0;
850
+ const contextWindow = typeof entry.context_length === "number" ? entry.context_length : typeof entry.context_window === "number" ? entry.context_window : void 0;
851
+ const inputTypes = normalizeModalities(entry.input_modalities);
852
+ const outputTypes = normalizeModalities(entry.output_modalities);
853
+ return {
854
+ provider: this.config.name,
855
+ id: entry.id,
856
+ displayName: entry.id,
857
+ ...typeof entry.name === "string" ? { name: entry.name } : {},
858
+ status: "available",
859
+ ...contextWindow !== void 0 ? { contextWindow } : {},
860
+ ...inputTypes !== void 0 ? { inputTypes } : {},
861
+ ...outputTypes !== void 0 ? { outputTypes } : {},
862
+ ...parameters !== void 0 ? {
863
+ supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
864
+ supportsStructuredOutput: parameters.includes("response_format"),
865
+ supportsStreaming: parameters.includes("stream")
866
+ } : {},
867
+ metadata: {
868
+ ...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
869
+ ...typeof entry.created === "number" ? { createdAt: entry.created } : {},
870
+ ...parameters !== void 0 ? { supportedParameters: parameters } : {}
871
+ }
872
+ };
873
+ }
874
+ /** Streaming where the gateway supports it (§11). */
875
+ stream(request, onChunk) {
876
+ return streamChatCompletion(this.config, request, onChunk, this.env);
877
+ }
878
+ /** Provider errors are part of the audit trail (§14 correlation, §18 observability). */
879
+ async recordProviderError(operation, error) {
880
+ await this.audit?.record({
881
+ at: (/* @__PURE__ */ new Date()).toISOString(),
882
+ action: "provider.error",
883
+ id: this.providerId,
884
+ providerId: this.providerId,
885
+ detail: {
886
+ operation,
887
+ code: error.code,
888
+ kind: error.details?.kind,
889
+ message: error.message
890
+ }
891
+ });
892
+ }
462
893
  async healthCheck() {
463
894
  const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
464
895
  try {
@@ -488,27 +919,42 @@ var OpenAICompatibleProvider = class {
488
919
  }
489
920
  }
490
921
  async chat(request) {
491
- const { status, body } = await requestJson(`${this.config.baseUrl}/chat/completions`, {
492
- method: "POST",
493
- headers: authHeaders(this.config),
494
- body: {
495
- model: request.model,
496
- messages: request.messages,
497
- ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
498
- ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
499
- ...request.stop !== void 0 ? { stop: request.stop } : {}
922
+ let response;
923
+ try {
924
+ response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
925
+ method: "POST",
926
+ headers: authHeaders(this.config, this.env),
927
+ timeoutMs: this.requestTimeoutMs,
928
+ body: {
929
+ model: request.model,
930
+ messages: request.messages,
931
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
932
+ ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
933
+ ...request.stop !== void 0 ? { stop: request.stop } : {}
934
+ }
935
+ });
936
+ } catch (error) {
937
+ if (error instanceof ProviderError) {
938
+ await this.recordProviderError("chat", error);
939
+ throw error;
500
940
  }
501
- });
502
- if (status === 401 || status === 403) throw authFailed(this.config, status);
503
- if (status === 404) {
504
- throw new ProviderError(
505
- "MODEL_NOT_FOUND",
506
- `Model "${request.model}" or the chat endpoint was not found on provider "${this.config.name}" (HTTP 404).`,
507
- { details: { status } }
508
- );
941
+ const normalized = providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
942
+ await this.recordProviderError("chat", normalized);
943
+ throw normalized;
509
944
  }
945
+ const { status, body } = response;
510
946
  if (status !== 200) {
511
- throw unavailable(this.config, `Chat completion returned HTTP ${status}.`, status);
947
+ const httpError = providerHttpError({
948
+ provider: this.config.name,
949
+ operation: "chat",
950
+ status,
951
+ body,
952
+ ...status === 404 ? {
953
+ code: "MODEL_NOT_FOUND"
954
+ } : {}
955
+ });
956
+ await this.recordProviderError("chat", httpError);
957
+ throw httpError;
512
958
  }
513
959
  const payload = body;
514
960
  const choices = Array.isArray(payload.choices) ? payload.choices : [];
@@ -530,6 +976,81 @@ var OpenAICompatibleProvider = class {
530
976
  };
531
977
  }
532
978
  };
979
+ async function streamChatCompletion(config, request, onChunk, env = process.env) {
980
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
981
+ const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
982
+ let response;
983
+ try {
984
+ response = await fetch(`${baseUrl}/chat/completions`, {
985
+ method: "POST",
986
+ headers: {
987
+ accept: "text/event-stream",
988
+ "content-type": "application/json",
989
+ ...authHeaders(config, env)
990
+ },
991
+ body: JSON.stringify({
992
+ model: request.model,
993
+ messages: request.messages,
994
+ stream: true,
995
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
996
+ ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
997
+ }),
998
+ signal: AbortSignal.timeout(timeoutMs)
999
+ });
1000
+ } catch (error) {
1001
+ throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
1002
+ }
1003
+ if (!response.ok) {
1004
+ const body = await response.json().catch(() => void 0);
1005
+ throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
1006
+ }
1007
+ if (!response.body) {
1008
+ throw providerHttpError({
1009
+ provider: config.name,
1010
+ operation: "stream",
1011
+ status: 500,
1012
+ body: { error: { message: "streaming is not supported by this endpoint" } }
1013
+ });
1014
+ }
1015
+ let content = "";
1016
+ let model = request.model;
1017
+ let finishReason;
1018
+ const decoder = new TextDecoder();
1019
+ let buffer = "";
1020
+ for await (const piece of response.body) {
1021
+ buffer += decoder.decode(piece, { stream: true });
1022
+ let newline = buffer.indexOf("\n");
1023
+ while (newline >= 0) {
1024
+ const line = buffer.slice(0, newline).trim();
1025
+ buffer = buffer.slice(newline + 1);
1026
+ newline = buffer.indexOf("\n");
1027
+ if (line.length === 0 || line.startsWith(":")) continue;
1028
+ const data = line.startsWith("data:") ? line.slice(5).trim() : line;
1029
+ if (data === "[DONE]") continue;
1030
+ let event;
1031
+ try {
1032
+ event = JSON.parse(data);
1033
+ } catch {
1034
+ continue;
1035
+ }
1036
+ if (typeof event.model === "string") model = event.model;
1037
+ const choices = Array.isArray(event.choices) ? event.choices : [];
1038
+ const first = choices[0];
1039
+ const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
1040
+ if (delta.length > 0) content += delta;
1041
+ if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
1042
+ onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
1043
+ }
1044
+ }
1045
+ return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
1046
+ }
1047
+ function normalizeModalities(value) {
1048
+ const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
1049
+ if (!list) return void 0;
1050
+ const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
1051
+ const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
1052
+ return modalities.length > 0 ? modalities : void 0;
1053
+ }
533
1054
  function normalizeBaseUrl(baseUrl) {
534
1055
  return baseUrl.replace(/\/+$/, "");
535
1056
  }
@@ -544,14 +1065,6 @@ function extractModelEntries(body, config) {
544
1065
  { details: { endpoint: "models" } }
545
1066
  );
546
1067
  }
547
- function authFailed(config, status) {
548
- const envHint = config.apiKeyEnvVar ? ` Check that $${config.apiKeyEnvVar} is set and valid.` : " The provider requires authentication; configure api_key_env_var for it.";
549
- return new ProviderError(
550
- "PROVIDER_AUTH_FAILED",
551
- `Authentication with provider "${config.name}" failed (HTTP ${status}).${envHint}`,
552
- { details: { status } }
553
- );
554
- }
555
1068
  function unavailable(config, message, status) {
556
1069
  return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
557
1070
  details: { status }
@@ -564,11 +1077,33 @@ function inferHealth(error) {
564
1077
 
565
1078
  // src/providers/omnihilbras/index.ts
566
1079
  var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
1080
+ gateway;
1081
+ async listModels() {
1082
+ const raw = await this.rawModelsResponse();
1083
+ await this.extractGatewayMetadata(raw);
1084
+ const models = await super.listModels();
1085
+ return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
1086
+ }
567
1087
  /**
568
1088
  * Run the full connect flow. Throws on authentication or transport failure
569
1089
  * (use healthCheck() for a non-throwing probe).
570
1090
  */
571
- async connect(registry) {
1091
+ /**
1092
+ * Gateway metadata reported alongside the model list (gateway version, tier,
1093
+ * region…), kept separate from per-model metadata (§11 — provider metadata).
1094
+ */
1095
+ gatewayMetadata() {
1096
+ return this.gateway;
1097
+ }
1098
+ async extractGatewayMetadata(body) {
1099
+ if (body !== null && typeof body === "object") {
1100
+ const gateway = body.gateway;
1101
+ if (gateway !== null && typeof gateway === "object") {
1102
+ this.gateway = gateway;
1103
+ }
1104
+ }
1105
+ }
1106
+ async connectAndRegister(registry) {
572
1107
  const apiKey = resolveApiKey(this.config);
573
1108
  const discovered = await this.listModels();
574
1109
  const seen = /* @__PURE__ */ new Set();
@@ -612,13 +1147,32 @@ var OPENAI_COMPATIBLE_TYPES = [
612
1147
  "local",
613
1148
  "custom"
614
1149
  ];
615
- function createProvider(config) {
1150
+ function assertProviderConfig(config) {
1151
+ if (!config.name || config.name.trim().length === 0) {
1152
+ throw new ConfigError("CONFIG_INVALID", "Provider name must not be empty.");
1153
+ }
1154
+ if (!config.baseUrl || config.baseUrl.trim().length === 0) {
1155
+ throw new ConfigError("CONFIG_INVALID", `Provider "${config.name}" needs a baseUrl.`);
1156
+ }
1157
+ try {
1158
+ new URL(config.baseUrl);
1159
+ } catch {
1160
+ throw new ConfigError("CONFIG_INVALID", `Provider "${config.name}" has an invalid baseUrl: ${config.baseUrl}`);
1161
+ }
1162
+ }
1163
+ function createProvider(config, options = {}) {
1164
+ assertProviderConfig(config);
1165
+ const shared = {
1166
+ ...options.env !== void 0 ? { env: options.env } : {},
1167
+ ...options.audit !== void 0 ? { audit: options.audit } : {}
1168
+ };
616
1169
  if (config.type === "omnihilbras") {
617
- return new OmniHilbrasProvider(config);
1170
+ return new OmniHilbrasProvider(config, void 0, options.env, options.audit);
618
1171
  }
619
1172
  if (OPENAI_COMPATIBLE_TYPES.includes(config.type)) {
620
- return new OpenAICompatibleProvider(config);
1173
+ return new OpenAICompatibleProvider(config, void 0, options.env, options.audit);
621
1174
  }
1175
+ void shared;
622
1176
  throw new OmniNodeError("NOT_IMPLEMENTED", `Provider type "${config.type}" has no adapter yet.`);
623
1177
  }
624
1178
 
@@ -672,20 +1226,420 @@ var ModelRegistry = class {
672
1226
  };
673
1227
 
674
1228
  // src/agents/process/index.ts
675
- import { spawn } from "child_process";
1229
+ import { spawn } from "child_process";
1230
+
1231
+ // src/agent-protocol/version.ts
1232
+ var PROTOCOL_V2 = "omninode-agent-protocol/2";
1233
+ var SUPPORTED_PROTOCOL_VERSIONS = [
1234
+ "omninode-agent-protocol/1",
1235
+ PROTOCOL_V2
1236
+ ];
1237
+ var MAX_MESSAGE_BYTES = 1048576;
1238
+ function isSupportedProtocol(protocol) {
1239
+ return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
1240
+ }
1241
+
1242
+ // src/agent-protocol/types.ts
1243
+ function newMessageId(prefix = "msg") {
1244
+ const random = Math.random().toString(36).slice(2, 10);
1245
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
1246
+ }
1247
+ function envelope(type, payload, ids = {}) {
1248
+ return {
1249
+ protocol: PROTOCOL_V2,
1250
+ messageId: newMessageId(),
1251
+ ...ids.requestId !== void 0 ? { requestId: ids.requestId } : {},
1252
+ ...ids.taskId !== void 0 ? { taskId: ids.taskId } : {},
1253
+ ...ids.agentId !== void 0 ? { agentId: ids.agentId } : {},
1254
+ ...ids.pipelineId !== void 0 ? { pipelineId: ids.pipelineId } : {},
1255
+ ...ids.executionId !== void 0 ? { executionId: ids.executionId } : {},
1256
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1257
+ type,
1258
+ payload,
1259
+ ...ids.metadata !== void 0 ? { metadata: ids.metadata } : {}
1260
+ };
1261
+ }
1262
+
1263
+ // src/agent-protocol/codec.ts
1264
+ var KNOWN_TYPES = /* @__PURE__ */ new Set([
1265
+ // node → agent
1266
+ "TASK",
1267
+ "HELLO_ACK",
1268
+ "CONTEXT",
1269
+ "INSTRUCTION",
1270
+ "RESPONSE",
1271
+ "CANCEL",
1272
+ // agent → node
1273
+ "HELLO",
1274
+ "TASK_REQUEST",
1275
+ "TASK_ACCEPTED",
1276
+ "TASK_STARTED",
1277
+ "STATUS",
1278
+ "QUESTION",
1279
+ "REPORT",
1280
+ "ARTIFACT",
1281
+ "COMPLETION",
1282
+ "ERROR",
1283
+ "CANCELLED"
1284
+ ]);
1285
+ var PAYLOAD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["TASK", "REPORT", "COMPLETION", "ERROR", "QUESTION", "ARTIFACT", "HELLO"]);
1286
+ function validateEnvelope(value, line) {
1287
+ const sample = line?.slice(0, 200);
1288
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1289
+ return { ok: false, error: { code: "NOT_AN_OBJECT", message: "message must be a JSON object", ...sample ? { sample } : {} } };
1290
+ }
1291
+ const raw = value;
1292
+ const protocol = raw.protocol;
1293
+ if (protocol === void 0) {
1294
+ const type = raw.type;
1295
+ if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
1296
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(type)}`, ...sample ? { sample } : {} } };
1297
+ }
1298
+ return {
1299
+ ok: true,
1300
+ legacy: true,
1301
+ envelope: {
1302
+ protocol: "omninode-agent-protocol/1",
1303
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "legacy",
1304
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1305
+ type,
1306
+ payload: raw.payload
1307
+ }
1308
+ };
1309
+ }
1310
+ if (typeof protocol !== "string" || !isSupportedProtocol(protocol)) {
1311
+ return {
1312
+ ok: false,
1313
+ error: { code: "UNSUPPORTED_VERSION", message: `unsupported protocol "${String(protocol)}"`, ...sample ? { sample } : {} }
1314
+ };
1315
+ }
1316
+ if (raw.type === void 0) {
1317
+ return { ok: false, error: { code: "MISSING_FIELD", message: "message is missing 'type'", ...sample ? { sample } : {} } };
1318
+ }
1319
+ if (typeof raw.type !== "string" || !KNOWN_TYPES.has(raw.type)) {
1320
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(raw.type)}`, ...sample ? { sample } : {} } };
1321
+ }
1322
+ if (raw.timestamp !== void 0 && typeof raw.timestamp !== "string") {
1323
+ return { ok: false, error: { code: "MISSING_FIELD", message: "'timestamp' must be a string", ...sample ? { sample } : {} } };
1324
+ }
1325
+ if (PAYLOAD_REQUIRED_TYPES.has(raw.type) && (raw.payload === void 0 || raw.payload === null)) {
1326
+ return { ok: false, error: { code: "INVALID_PAYLOAD", message: `${raw.type} requires a payload`, ...sample ? { sample } : {} } };
1327
+ }
1328
+ return {
1329
+ ok: true,
1330
+ legacy: protocol !== PROTOCOL_V2,
1331
+ envelope: {
1332
+ protocol,
1333
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "unknown",
1334
+ ...typeof raw.requestId === "string" ? { requestId: raw.requestId } : {},
1335
+ ...typeof raw.taskId === "string" ? { taskId: raw.taskId } : {},
1336
+ ...typeof raw.agentId === "string" ? { agentId: raw.agentId } : {},
1337
+ ...typeof raw.pipelineId === "string" ? { pipelineId: raw.pipelineId } : {},
1338
+ ...typeof raw.executionId === "string" ? { executionId: raw.executionId } : {},
1339
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
1340
+ type: raw.type,
1341
+ payload: raw.payload,
1342
+ ...raw.metadata !== null && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}
1343
+ }
1344
+ };
1345
+ }
1346
+ function decodeMessage(line, maxBytes = MAX_MESSAGE_BYTES) {
1347
+ const trimmed = line.trim();
1348
+ if (trimmed.length === 0) {
1349
+ return { ok: false, error: { code: "INVALID_JSON", message: "empty line" } };
1350
+ }
1351
+ if (trimmed.length > maxBytes) {
1352
+ return {
1353
+ ok: false,
1354
+ error: { code: "OVERSIZED", message: `message of ${trimmed.length} bytes exceeds the ${maxBytes} byte limit`, sample: trimmed.slice(0, 200) }
1355
+ };
1356
+ }
1357
+ let value;
1358
+ try {
1359
+ value = JSON.parse(trimmed);
1360
+ } catch (error) {
1361
+ return {
1362
+ ok: false,
1363
+ error: { code: "INVALID_JSON", message: error instanceof Error ? error.message : "invalid JSON", sample: trimmed.slice(0, 200) }
1364
+ };
1365
+ }
1366
+ return validateEnvelope(value, trimmed);
1367
+ }
1368
+ function encodeMessage(envelope2, maxBytes = MAX_MESSAGE_BYTES) {
1369
+ const line = JSON.stringify(envelope2);
1370
+ if (line.length > maxBytes) {
1371
+ throw new Error(`outbound protocol message of ${line.length} bytes exceeds the ${maxBytes} byte limit`);
1372
+ }
1373
+ return line;
1374
+ }
1375
+ var ProtocolDecoder = class {
1376
+ buffer = "";
1377
+ maxBytes;
1378
+ constructor(maxBytes = MAX_MESSAGE_BYTES) {
1379
+ this.maxBytes = maxBytes;
1380
+ }
1381
+ push(chunk) {
1382
+ this.buffer += chunk;
1383
+ const results = [];
1384
+ let index = this.buffer.indexOf("\n");
1385
+ while (index >= 0) {
1386
+ const line = this.buffer.slice(0, index);
1387
+ this.buffer = this.buffer.slice(index + 1);
1388
+ if (line.trim().length > 0) results.push(decodeMessage(line, this.maxBytes));
1389
+ index = this.buffer.indexOf("\n");
1390
+ }
1391
+ if (this.buffer.length > this.maxBytes) {
1392
+ results.push({
1393
+ ok: false,
1394
+ error: { code: "OVERSIZED", message: `line exceeded ${this.maxBytes} bytes without a newline`, sample: this.buffer.slice(0, 200) }
1395
+ });
1396
+ this.buffer = "";
1397
+ }
1398
+ return results;
1399
+ }
1400
+ /** Flush any trailing line when the process exits. */
1401
+ flush() {
1402
+ if (this.buffer.trim().length === 0) {
1403
+ this.buffer = "";
1404
+ return [];
1405
+ }
1406
+ const result = decodeMessage(this.buffer, this.maxBytes);
1407
+ this.buffer = "";
1408
+ return [result];
1409
+ }
1410
+ };
1411
+
1412
+ // src/agent-protocol/handshake.ts
1413
+ function parseHello(envelopeValue) {
1414
+ if (envelopeValue.type !== "HELLO") {
1415
+ return { ok: false, reason: `expected HELLO, received ${envelopeValue.type}` };
1416
+ }
1417
+ const payload = envelopeValue.payload;
1418
+ if (payload === null || typeof payload !== "object") {
1419
+ return { ok: false, reason: "HELLO payload must be an object" };
1420
+ }
1421
+ const p = payload;
1422
+ if (typeof p.name !== "string" || p.name.length === 0) {
1423
+ return { ok: false, reason: "HELLO payload is missing 'name'" };
1424
+ }
1425
+ return {
1426
+ ok: true,
1427
+ descriptor: {
1428
+ protocol: envelopeValue.protocol,
1429
+ name: p.name,
1430
+ ...typeof p.version === "string" ? { version: p.version } : {},
1431
+ capabilities: Array.isArray(p.capabilities) ? p.capabilities.filter((c) => typeof c === "string") : [],
1432
+ supports: Array.isArray(p.supports) ? p.supports.filter((s) => typeof s === "string") : []
1433
+ }
1434
+ };
1435
+ }
1436
+ function buildHello(descriptor) {
1437
+ return envelope("HELLO", descriptor, { agentId: descriptor.name });
1438
+ }
1439
+ function buildHelloAck(descriptor) {
1440
+ return envelope("HELLO_ACK", {
1441
+ ok: true,
1442
+ protocol: PROTOCOL_V2,
1443
+ agent: descriptor.name,
1444
+ ...descriptor.version !== void 0 ? { agentVersion: descriptor.version } : {},
1445
+ supportedProtocols: [PROTOCOL_V2]
1446
+ }, { agentId: descriptor.name });
1447
+ }
1448
+
1449
+ // src/agent-protocol/session.ts
1450
+ var ProtocolSession = class {
1451
+ decoder;
1452
+ ids;
1453
+ responder;
1454
+ violations = [];
1455
+ questions = [];
1456
+ artifacts = [];
1457
+ reports = [];
1458
+ descriptor;
1459
+ completionSummary;
1460
+ errorMessage;
1461
+ constructor(ids, options = {}) {
1462
+ this.ids = ids;
1463
+ this.responder = options.responder;
1464
+ this.decoder = new ProtocolDecoder(options.maxBytes);
1465
+ }
1466
+ /** The v2 messages OmniNode sends to open a task (written to the agent's stdin). */
1467
+ taskMessages(objective, role, context, instruction) {
1468
+ const ids = this.ids;
1469
+ const messages = [envelope("TASK", { taskId: ids.taskId, objective }, ids)];
1470
+ if (role !== void 0) messages.push(envelope("CONTEXT", { role }, ids));
1471
+ if (context !== void 0 && context.length > 0) messages.push(envelope("CONTEXT", { body: context }, ids));
1472
+ if (instruction !== void 0 && instruction.length > 0) messages.push(envelope("INSTRUCTION", { text: instruction }, ids));
1473
+ return messages.map((m) => encodeMessage(m));
1474
+ }
1475
+ helloAck(descriptor) {
1476
+ return encodeMessage(buildHelloAck(descriptor));
1477
+ }
1478
+ /** Consumes decoded stdout, answering questions as they arrive. */
1479
+ async handle(decoded) {
1480
+ if (!decoded.ok) {
1481
+ this.violations.push(decoded.error);
1482
+ return;
1483
+ }
1484
+ const message = decoded.envelope;
1485
+ if (message.protocol === PROTOCOL_V2) this.sawV2 = true;
1486
+ if (message.type === "HELLO") {
1487
+ const hello = parseHello(message);
1488
+ if (hello.ok) this.descriptor = hello.descriptor;
1489
+ else this.violations.push({ code: "INVALID_PAYLOAD", message: `bad HELLO: ${hello.reason}` });
1490
+ return;
1491
+ }
1492
+ if (message.type === "QUESTION") {
1493
+ await this.answerQuestion(message);
1494
+ return;
1495
+ }
1496
+ if (message.type === "REPORT") {
1497
+ this.reports.push(message.payload);
1498
+ return;
1499
+ }
1500
+ if (message.type === "ARTIFACT") {
1501
+ this.artifacts.push(message.payload);
1502
+ return;
1503
+ }
1504
+ if (message.type === "COMPLETION") {
1505
+ const payload = message.payload;
1506
+ this.completionSummary = typeof payload?.summary === "string" ? payload.summary : "";
1507
+ return;
1508
+ }
1509
+ if (message.type === "ERROR") {
1510
+ const payload = message.payload;
1511
+ const detail = typeof payload?.message === "string" ? payload.message : "agent reported an error";
1512
+ const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
1513
+ this.errorMessage = `Agent error${code}: ${detail}`;
1514
+ return;
1515
+ }
1516
+ if (message.type === "TASK_ACCEPTED" || message.type === "TASK_STARTED" || message.type === "STATUS") {
1517
+ return;
1518
+ }
1519
+ this.violations.push({
1520
+ code: "UNKNOWN_TYPE",
1521
+ message: `unexpected message type ${message.type} from agent`
1522
+ });
1523
+ }
1524
+ async answerQuestion(message) {
1525
+ const payload = message.payload ?? {};
1526
+ const questionId = typeof payload.questionId === "string" ? payload.questionId : message.messageId;
1527
+ const question = typeof payload.question === "string" ? payload.question : "";
1528
+ const expected = typeof payload.expected === "string" ? payload.expected : "text";
1529
+ let answer;
1530
+ if (this.responder) {
1531
+ try {
1532
+ answer = await this.responder({ questionId, question, expected });
1533
+ } catch (error) {
1534
+ this.violations.push({
1535
+ code: "INVALID_PAYLOAD",
1536
+ message: `question responder failed: ${error instanceof Error ? error.message : String(error)}`
1537
+ });
1538
+ }
1539
+ }
1540
+ this.questions.push({ questionId, question, ...answer !== void 0 ? { answer } : {} });
1541
+ this.pendingAnswers.push(
1542
+ encodeMessage(
1543
+ envelope("RESPONSE", { questionId, ...answer !== void 0 ? { body: answer } : {} }, {
1544
+ ...this.ids,
1545
+ requestId: message.requestId ?? this.ids.requestId
1546
+ })
1547
+ )
1548
+ );
1549
+ }
1550
+ pendingAnswers = [];
1551
+ /** True once any inbound message carried the v2 protocol field. */
1552
+ sawV2 = false;
1553
+ };
1554
+
1555
+ // src/agents/cwd.ts
1556
+ import { statSync } from "fs";
1557
+ import { isAbsolute, relative, resolve } from "path";
1558
+ function validateWorkingDirectory(cwd, options = {}) {
1559
+ if (!cwd) return void 0;
1560
+ const normalized = isAbsolute(cwd) ? resolve(cwd) : resolve(process.cwd(), cwd);
1561
+ let stats;
1562
+ try {
1563
+ stats = statSync(normalized);
1564
+ } catch (error) {
1565
+ throw new AgentError(
1566
+ "AGENT_NOT_FOUND",
1567
+ `Working directory "${normalized}" does not exist or is not accessible.`,
1568
+ { cause: error, details: { cwd: normalized } }
1569
+ );
1570
+ }
1571
+ if (!stats.isDirectory()) {
1572
+ throw new AgentError("AGENT_NOT_FOUND", `Working directory "${normalized}" is not a directory.`, {
1573
+ details: { cwd: normalized }
1574
+ });
1575
+ }
1576
+ try {
1577
+ statSync(resolve(normalized, "."));
1578
+ } catch (error) {
1579
+ throw new AgentError(
1580
+ "AGENT_NOT_FOUND",
1581
+ `Working directory "${normalized}" is not accessible.`,
1582
+ { cause: error }
1583
+ );
1584
+ }
1585
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
1586
+ const rel = relative(projectRoot, normalized);
1587
+ const outsideProject = rel.startsWith("..") || isAbsolute(rel);
1588
+ if (outsideProject && options.allowExternal !== true) {
1589
+ throw new AgentError(
1590
+ "AGENT_NOT_FOUND",
1591
+ `Working directory "${normalized}" is outside the project root "${projectRoot}". Set allow_external_cwd: true on the agent to permit this.`,
1592
+ { details: { cwd: normalized, projectRoot } }
1593
+ );
1594
+ }
1595
+ return { cwd: normalized, outsideProject };
1596
+ }
1597
+
1598
+ // src/agents/env.ts
1599
+ var BASELINE_KEYS = ["PATH", "HOME"];
1600
+ function resolveEnvPolicy(config) {
1601
+ if (config.envPolicy !== void 0) return config.envPolicy;
1602
+ if (config.inheritEnv === false) return "explicit";
1603
+ return "inherit";
1604
+ }
1605
+ function buildChildEnv(config) {
1606
+ const configured = config.env ?? {};
1607
+ const policy = resolveEnvPolicy(config);
1608
+ if (policy === "inherit") {
1609
+ return { ...process.env, ...configured };
1610
+ }
1611
+ const base = {};
1612
+ for (const key of BASELINE_KEYS) {
1613
+ const value = process.env[key];
1614
+ if (value !== void 0) base[key] = value;
1615
+ }
1616
+ switch (policy) {
1617
+ case "explicit":
1618
+ return { ...base, ...configured };
1619
+ case "allowlist": {
1620
+ const allowed = { ...base };
1621
+ for (const key of config.envAllowlist ?? []) {
1622
+ const value = process.env[key];
1623
+ if (value !== void 0) allowed[key] = value;
1624
+ }
1625
+ return { ...allowed, ...configured };
1626
+ }
1627
+ case "denylist": {
1628
+ const denied = new Set((config.envDenylist ?? []).map((k) => k.toUpperCase()));
1629
+ const filtered = {};
1630
+ for (const [key, value] of Object.entries(process.env)) {
1631
+ if (denied.has(key.toUpperCase())) continue;
1632
+ filtered[key] = value;
1633
+ }
1634
+ return { ...filtered, ...configured };
1635
+ }
1636
+ }
1637
+ }
1638
+
1639
+ // src/agents/process/index.ts
676
1640
  var DEFAULT_TIMEOUT_MS2 = 6e5;
677
1641
  var KILL_GRACE_MS = 5e3;
678
- function buildChildEnv(config) {
679
- const configured = config.env ?? {};
680
- if (config.inheritEnv !== false) {
681
- return { ...process.env, ...configured };
682
- }
683
- return {
684
- PATH: process.env.PATH ?? "",
685
- HOME: process.env.HOME ?? "",
686
- ...configured
687
- };
688
- }
1642
+ var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
689
1643
  function composeTaskText(input) {
690
1644
  const sections = [`# Objective
691
1645
  ${input.objective}`];
@@ -706,18 +1660,19 @@ ${input.instruction}`);
706
1660
  return sections.join("\n\n");
707
1661
  }
708
1662
  function buildProtocolMessages(input, agentName) {
709
- const meta = {
1663
+ const session = new ProtocolSession({
1664
+ requestId: `req-${input.taskId}`,
710
1665
  taskId: input.taskId,
711
- agent: agentName,
712
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
713
- };
714
- const messages = [
715
- { type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
716
- ];
717
- if (input.role) messages.push({ type: "ROLE", payload: input.role });
718
- if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
719
- if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
720
- return messages.map((message) => JSON.stringify({ meta, message }));
1666
+ agentId: agentName,
1667
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1668
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1669
+ });
1670
+ return session.taskMessages(
1671
+ input.objective,
1672
+ input.role,
1673
+ input.context,
1674
+ input.instruction
1675
+ );
721
1676
  }
722
1677
  var ProcessAgent = class {
723
1678
  config;
@@ -746,40 +1701,76 @@ var ProcessAgent = class {
746
1701
  }
747
1702
  const mode = this.config.inputMode ?? "stdin";
748
1703
  const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
749
- return new Promise((resolve, reject) => {
1704
+ return new Promise((resolve2, reject) => {
1705
+ let cwd;
1706
+ try {
1707
+ cwd = validateWorkingDirectory(this.config.cwd, {
1708
+ allowExternal: this.config.allowExternalCwd
1709
+ })?.cwd;
1710
+ } catch (error) {
1711
+ reject(error);
1712
+ return;
1713
+ }
750
1714
  const child = spawn(
751
1715
  command,
752
1716
  [...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
753
1717
  {
754
- cwd: this.config.cwd,
1718
+ ...cwd !== void 0 ? { cwd } : {},
755
1719
  env: buildChildEnv(this.config),
756
- stdio: ["pipe", "pipe", "pipe"]
1720
+ stdio: ["pipe", "pipe", "pipe"],
1721
+ // Own process group so the whole tree can be terminated together
1722
+ // (roadmap §8: no orphaned children).
1723
+ ...process.platform === "win32" ? {} : { detached: true }
757
1724
  }
758
1725
  );
759
1726
  this.running.set(input.taskId, child);
1727
+ GLOBAL_RUNNING.add(child);
1728
+ installSignalForwarding();
760
1729
  let stdout = "";
761
1730
  let stderr = "";
762
1731
  let timedOut = false;
1732
+ let outputLimitExceeded = false;
763
1733
  let settled = false;
1734
+ const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
764
1735
  const timer = setTimeout(() => {
765
1736
  timedOut = true;
766
- this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
767
- child.kill("SIGTERM");
768
- setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
1737
+ this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process tree.`);
1738
+ killTree(child, "SIGTERM");
1739
+ setTimeout(() => killTree(child, "SIGKILL"), KILL_GRACE_MS).unref();
769
1740
  }, timeoutMs);
770
1741
  const settle = (finish) => {
771
1742
  if (settled) return;
772
1743
  settled = true;
773
1744
  clearTimeout(timer);
774
1745
  this.running.delete(input.taskId);
1746
+ GLOBAL_RUNNING.delete(child);
1747
+ child.stdin?.destroy();
1748
+ child.stdout?.destroy();
1749
+ child.stderr?.destroy();
775
1750
  finish();
776
1751
  };
777
- child.stdout?.on("data", (chunk) => {
778
- stdout += chunk.toString();
779
- });
780
- child.stderr?.on("data", (chunk) => {
781
- stderr += chunk.toString();
782
- });
1752
+ const collect = (into, chunk) => {
1753
+ if (outputLimitExceeded) return;
1754
+ const text = sanitizeChunk(chunk);
1755
+ if (into === "stdout") {
1756
+ if (stdout.length + text.length > maxOutputBytes) {
1757
+ stdout = (stdout + text).slice(0, maxOutputBytes);
1758
+ outputLimitExceeded = true;
1759
+ this.log.warn(`Agent "${this.config.name}" exceeded the ${maxOutputBytes} byte output limit \u2014 killing it.`);
1760
+ killTree(child, "SIGKILL");
1761
+ return;
1762
+ }
1763
+ stdout += text;
1764
+ } else {
1765
+ if (stderr.length + text.length > maxOutputBytes) {
1766
+ stderr = (stderr + text).slice(0, maxOutputBytes);
1767
+ return;
1768
+ }
1769
+ stderr += text;
1770
+ }
1771
+ };
1772
+ child.stdout?.on("data", (chunk) => collect("stdout", chunk));
1773
+ child.stderr?.on("data", (chunk) => collect("stderr", chunk));
783
1774
  child.stdin?.on("error", () => {
784
1775
  });
785
1776
  if (mode !== "arg") {
@@ -833,7 +1824,7 @@ var ProcessAgent = class {
833
1824
  const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
834
1825
  if (mode === "protocol") {
835
1826
  try {
836
- resolve(
1827
+ resolve2(
837
1828
  interpretProtocolRun({
838
1829
  input,
839
1830
  agentName: this.config.name,
@@ -848,14 +1839,14 @@ var ProcessAgent = class {
848
1839
  return;
849
1840
  }
850
1841
  const trimmed = stdout.trim();
851
- resolve({
1842
+ resolve2({
852
1843
  taskId: input.taskId,
853
- status: code === 0 ? "completed" : "failed",
1844
+ status: code === 0 && !outputLimitExceeded ? "completed" : "failed",
854
1845
  summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
855
1846
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
856
1847
  exitCode: code ?? void 0,
857
1848
  logs,
858
- error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
1849
+ ...outputLimitExceeded ? { error: `Agent exceeded the ${maxOutputBytes} byte output limit and was terminated.` } : code === 0 ? {} : { error: describeExit(code, signal) }
859
1850
  });
860
1851
  });
861
1852
  });
@@ -864,67 +1855,117 @@ var ProcessAgent = class {
864
1855
  /** Ask the agent to stop current work (SIGTERM). */
865
1856
  async cancel(taskId) {
866
1857
  const child = this.running.get(taskId);
867
- if (child) child.kill("SIGTERM");
1858
+ if (child) killTree(child, "SIGTERM");
868
1859
  }
869
1860
  runningTaskIds() {
870
1861
  return [...this.running.keys()];
871
1862
  }
872
1863
  };
873
- function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
874
- const reports = [];
875
- let summary;
876
- let error;
877
- let sawProtocolMessage = false;
878
- for (const line of stdout.split("\n")) {
879
- const trimmed2 = line.trim();
880
- if (trimmed2.length === 0) continue;
881
- const message = parseAgentMessage(trimmed2);
882
- if (!message) continue;
883
- sawProtocolMessage = true;
884
- if (message.type === "REPORT") {
885
- const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
886
- if (report) reports.push(report);
887
- } else if (message.type === "COMPLETION") {
888
- const payload = message.payload;
889
- if (typeof payload?.summary === "string") summary = payload.summary;
890
- } else if (message.type === "ERROR") {
891
- const payload = message.payload;
892
- const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
893
- const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
894
- error = `Agent error${code}: ${detail}`;
895
- }
1864
+ function interpretProtocolRun({
1865
+ input,
1866
+ agentName,
1867
+ stdout,
1868
+ exitCode,
1869
+ logs,
1870
+ responder
1871
+ }) {
1872
+ const session = new ProtocolSession(
1873
+ {
1874
+ requestId: `req-${input.taskId}`,
1875
+ taskId: input.taskId,
1876
+ agentId: agentName,
1877
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1878
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1879
+ },
1880
+ { ...responder !== void 0 ? { responder } : {} }
1881
+ );
1882
+ for (const result of session.decoder.push(stdout)) {
1883
+ session.handle(result);
896
1884
  }
897
- if (!sawProtocolMessage && exitCode === 0) {
898
- throw new ProtocolError(
899
- `Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
900
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
901
- );
1885
+ for (const result of session.decoder.flush()) {
1886
+ session.handle(result);
902
1887
  }
1888
+ const reports = session.reports.flatMap((payload) => Array.isArray(payload) ? payload : [payload]).map((payload, index) => normalizeReport(payload, input.taskId, agentName, index)).filter((report) => report !== void 0);
903
1889
  const trimmed = stdout.trim();
1890
+ const protocolBlock = {
1891
+ protocol: session.descriptor?.protocol ?? PROTOCOL_V2,
1892
+ legacy: !session.sawV2 && session.descriptor === void 0,
1893
+ ...session.descriptor !== void 0 ? { descriptor: session.descriptor } : {},
1894
+ questions: session.questions,
1895
+ violations: session.violations.map((v) => `${v.code}: ${v.message}`)
1896
+ };
1897
+ if (session.errorMessage !== void 0) {
1898
+ return {
1899
+ taskId: input.taskId,
1900
+ status: "failed",
1901
+ error: session.errorMessage,
1902
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1903
+ exitCode: exitCode ?? void 0,
1904
+ logs,
1905
+ protocol: protocolBlock,
1906
+ ...reports.length > 0 ? { reports } : {}
1907
+ };
1908
+ }
1909
+ if (session.descriptor === void 0 && session.violations.length > 0 && session.reports.length === 0 && session.completionSummary === void 0) {
1910
+ return {
1911
+ taskId: input.taskId,
1912
+ status: "failed",
1913
+ error: `agent produced no valid protocol messages: ${protocolBlock.violations.join("; ")}`,
1914
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1915
+ exitCode: exitCode ?? void 0,
1916
+ logs,
1917
+ protocol: protocolBlock
1918
+ };
1919
+ }
1920
+ const completed = exitCode === 0 && session.completionSummary !== void 0;
904
1921
  return {
905
1922
  taskId: input.taskId,
906
- status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
907
- summary: summary ?? reports[0]?.summary,
1923
+ status: completed || exitCode === 0 ? completed ? "completed" : "completed" : "failed",
1924
+ summary: session.completionSummary ?? reports[0]?.summary,
908
1925
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
909
1926
  exitCode: exitCode ?? void 0,
910
1927
  logs,
911
- reports: reports.length > 0 ? reports : void 0,
912
- error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
1928
+ protocol: protocolBlock,
1929
+ ...reports.length > 0 ? { reports } : {},
1930
+ ...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
913
1931
  };
914
1932
  }
915
- function parseAgentMessage(line) {
916
- try {
917
- const parsed = JSON.parse(line);
918
- if (typeof parsed.type === "string") {
919
- return { type: parsed.type, payload: parsed.payload };
920
- }
921
- const inner = parsed.message;
922
- if (inner && typeof inner.type === "string") {
923
- return { type: inner.type, payload: inner.payload };
1933
+ var GLOBAL_RUNNING = /* @__PURE__ */ new Set();
1934
+ var signalForwardingInstalled = false;
1935
+ function installSignalForwarding() {
1936
+ if (signalForwardingInstalled) return;
1937
+ signalForwardingInstalled = true;
1938
+ for (const signal of ["SIGINT", "SIGTERM"]) {
1939
+ process.on(signal, () => {
1940
+ for (const child of GLOBAL_RUNNING) {
1941
+ killTree(child, "SIGTERM");
1942
+ }
1943
+ });
1944
+ }
1945
+ }
1946
+ function killTree(child, signal) {
1947
+ if (child.pid === void 0) return;
1948
+ if (process.platform !== "win32") {
1949
+ try {
1950
+ process.kill(-child.pid, signal);
1951
+ return;
1952
+ } catch {
924
1953
  }
1954
+ }
1955
+ try {
1956
+ child.kill(signal);
925
1957
  } catch {
926
1958
  }
927
- return void 0;
1959
+ }
1960
+ function describeExit(code, signal) {
1961
+ if (signal !== null) {
1962
+ return signal === "SIGKILL" ? "Process was killed (SIGKILL) \u2014 usually an external kill or OOM." : `Process terminated by signal ${signal}.`;
1963
+ }
1964
+ if (code === null) return "Process terminated without an exit code.";
1965
+ return `Process exited with code ${code}.`;
1966
+ }
1967
+ function sanitizeChunk(chunk) {
1968
+ return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
928
1969
  }
929
1970
  function normalizeReport(payload, taskId, agentName, index) {
930
1971
  if (payload === null || typeof payload !== "object") return void 0;
@@ -945,6 +1986,40 @@ function firstLine(text, maxLength = 200) {
945
1986
  return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
946
1987
  }
947
1988
 
1989
+ // src/agents/adapters.ts
1990
+ var AgentAdapterRegistry = class {
1991
+ adapters = /* @__PURE__ */ new Map();
1992
+ register(adapter) {
1993
+ this.adapters.set(adapter.integration, adapter);
1994
+ }
1995
+ get(integration) {
1996
+ return this.adapters.get(integration);
1997
+ }
1998
+ list() {
1999
+ return [...this.adapters.values()];
2000
+ }
2001
+ create(config) {
2002
+ const adapter = this.get(config.integration);
2003
+ if (!adapter) {
2004
+ throw new AgentError(
2005
+ "AGENT_NOT_FOUND",
2006
+ `No agent adapter registered for integration "${config.integration}". Registered: ${[...this.adapters.keys()].join(", ") || "(none)"}.`
2007
+ );
2008
+ }
2009
+ return adapter.create(config);
2010
+ }
2011
+ };
2012
+ var ProcessAgentAdapter = class {
2013
+ constructor(factory) {
2014
+ this.factory = factory;
2015
+ }
2016
+ factory;
2017
+ integration = "process";
2018
+ create(config) {
2019
+ return this.factory(config);
2020
+ }
2021
+ };
2022
+
948
2023
  // src/agents/registry.ts
949
2024
  var AgentRegistry = class {
950
2025
  agents = /* @__PURE__ */ new Map();
@@ -960,14 +2035,24 @@ var AgentRegistry = class {
960
2035
  };
961
2036
 
962
2037
  // src/agents/factory.ts
963
- function createAgent(config) {
964
- if (config.integration === "process") {
965
- return new ProcessAgent(config);
2038
+ var defaultAgentAdapters = new AgentAdapterRegistry();
2039
+ defaultAgentAdapters.register(new ProcessAgentAdapter((config) => new ProcessAgent(config)));
2040
+ function assertAgentConfig(config) {
2041
+ if (!config.name || config.name.trim().length === 0) {
2042
+ throw new ConfigError("CONFIG_INVALID", "Agent name must not be empty.");
2043
+ }
2044
+ if (config.integration === "process" && (!config.command || config.command.trim().length === 0)) {
2045
+ throw new ConfigError("CONFIG_INVALID", `Agent "${config.name}" needs a command for the process adapter.`);
966
2046
  }
2047
+ }
2048
+ function createAgent(config, adapters = defaultAgentAdapters) {
2049
+ assertAgentConfig(config);
2050
+ const registered = adapters.get(config.integration);
2051
+ if (registered) return registered.create(config);
967
2052
  if (config.integration === "native") {
968
2053
  throw new OmniNodeError(
969
2054
  "NOT_IMPLEMENTED",
970
- `Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
2055
+ `No native adapter is registered for "${config.name}". Register an IAgentAdapter (createAgent(config, adapters)) or use input over stdin/stdout (type "cli").`
971
2056
  );
972
2057
  }
973
2058
  throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
@@ -1009,51 +2094,127 @@ function buildRoleRegistry(config) {
1009
2094
  }
1010
2095
 
1011
2096
  // src/persistence/json-file-store.ts
1012
- import { mkdir, readFile, rename, writeFile } from "fs/promises";
2097
+ import { open, mkdir, readFile, rename, stat, writeFile } from "fs/promises";
1013
2098
  import { dirname } from "path";
2099
+ var SCHEMA_VERSION = 2;
2100
+ async function writeFileAtomic(target, contents, durability = "rename") {
2101
+ const tmp = `${target}.tmp`;
2102
+ await mkdir(dirname(target), { recursive: true });
2103
+ if (durability === "fsync") {
2104
+ const handle = await open(tmp, "w");
2105
+ try {
2106
+ await handle.writeFile(contents, "utf8");
2107
+ await handle.sync();
2108
+ } finally {
2109
+ await handle.close();
2110
+ }
2111
+ } else {
2112
+ await writeFile(tmp, contents, "utf8");
2113
+ }
2114
+ await rename(tmp, target);
2115
+ if (durability === "fsync") {
2116
+ try {
2117
+ const dir = await open(dirname(target), "r");
2118
+ try {
2119
+ await dir.sync();
2120
+ } finally {
2121
+ await dir.close();
2122
+ }
2123
+ } catch {
2124
+ }
2125
+ }
2126
+ }
2127
+ async function inspectDocument(path2) {
2128
+ let raw;
2129
+ try {
2130
+ await stat(path2);
2131
+ } catch {
2132
+ return { status: "missing" };
2133
+ }
2134
+ try {
2135
+ raw = await readFile(path2, "utf8");
2136
+ } catch (error) {
2137
+ return { status: "corrupt", detail: `unreadable: ${error instanceof Error ? error.message : String(error)}` };
2138
+ }
2139
+ if (raw.trim().length === 0) return { status: "empty" };
2140
+ let parsed;
2141
+ try {
2142
+ parsed = JSON.parse(raw);
2143
+ } catch (error) {
2144
+ return { status: "corrupt", detail: error instanceof Error ? error.message : "invalid JSON" };
2145
+ }
2146
+ if (Array.isArray(parsed)) return { status: "v1-legacy" };
2147
+ if (parsed === null || typeof parsed !== "object") return { status: "corrupt", detail: "not an object" };
2148
+ const doc = parsed;
2149
+ if (!Array.isArray(doc.items)) return { status: "corrupt", detail: "missing items array" };
2150
+ const version = typeof doc.schemaVersion === "number" ? doc.schemaVersion : 1;
2151
+ if (version > SCHEMA_VERSION) return { status: "future", schemaVersion: version };
2152
+ if (version < SCHEMA_VERSION) return { status: "v1-legacy" };
2153
+ return { status: "current", schemaVersion: version, count: doc.items.length };
2154
+ }
2155
+ async function readDocument(path2, onCorrupt, quarantine = true) {
2156
+ const state = await inspectDocument(path2);
2157
+ if (state.status === "missing" || state.status === "empty") return [];
2158
+ if (state.status === "future") {
2159
+ const error = new Error(
2160
+ `${path2} was written by a newer OmniNode (schema v${state.schemaVersion} > v${SCHEMA_VERSION})`
2161
+ );
2162
+ onCorrupt?.(path2, error);
2163
+ return [];
2164
+ }
2165
+ let raw;
2166
+ try {
2167
+ raw = await readFile(path2, "utf8");
2168
+ } catch (error) {
2169
+ onCorrupt?.(path2, error);
2170
+ return [];
2171
+ }
2172
+ try {
2173
+ const parsed = JSON.parse(raw);
2174
+ if (Array.isArray(parsed)) return parsed;
2175
+ return parsed.items ?? [];
2176
+ } catch (error) {
2177
+ if (quarantine) {
2178
+ const quarantinePath = `${path2}.corrupt-${Date.now()}`;
2179
+ try {
2180
+ await rename(path2, quarantinePath);
2181
+ logger.child({ component: "persistence" }).warn(
2182
+ `Quarantined unreadable store file ${path2} \u2192 ${quarantinePath}`
2183
+ );
2184
+ } catch {
2185
+ }
2186
+ }
2187
+ onCorrupt?.(path2, error);
2188
+ return [];
2189
+ }
2190
+ }
1014
2191
  var JsonFileStore = class {
1015
2192
  filePath;
1016
2193
  schemaVersion;
2194
+ durability;
1017
2195
  onCorrupt;
2196
+ quarantine;
1018
2197
  log;
1019
2198
  queue = Promise.resolve();
1020
2199
  constructor(options) {
1021
2200
  this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
1022
- this.schemaVersion = options.schemaVersion ?? 1;
2201
+ this.schemaVersion = options.schemaVersion ?? SCHEMA_VERSION;
2202
+ this.durability = options.durability ?? "rename";
1023
2203
  this.onCorrupt = options.onCorrupt;
2204
+ this.quarantine = options.quarantine ?? true;
1024
2205
  this.log = logger.child({ component: "persistence", file: options.fileName });
1025
2206
  }
1026
2207
  get path() {
1027
2208
  return this.filePath;
1028
2209
  }
1029
- /** All items, or [] when missing/corrupt. */
2210
+ /** All items, or [] when missing/migrated-from-v1/corrupt. */
1030
2211
  async readAll() {
1031
- let raw;
1032
- try {
1033
- raw = await readFile(this.filePath, "utf8");
1034
- } catch {
1035
- return [];
1036
- }
1037
- try {
1038
- const parsed = JSON.parse(raw);
1039
- if (Array.isArray(parsed)) return parsed;
1040
- if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.items)) {
1041
- return parsed.items;
1042
- }
1043
- throw new Error("persisted file has an unrecognized shape");
1044
- } catch (error) {
1045
- this.log.warn(`Treating ${this.filePath} as empty \u2014 it could not be parsed.`);
1046
- this.onCorrupt?.(this.filePath, error);
1047
- return [];
1048
- }
2212
+ return readDocument(this.filePath, this.onCorrupt, this.quarantine);
1049
2213
  }
1050
2214
  async writeAll(items) {
1051
2215
  const document = { schemaVersion: this.schemaVersion, items };
1052
- const tmp = `${this.filePath}.tmp`;
1053
- await mkdir(dirname(this.filePath), { recursive: true });
1054
- await writeFile(tmp, `${JSON.stringify(document, null, 2)}
1055
- `, "utf8");
1056
- await rename(tmp, this.filePath);
2216
+ await writeFileAtomic(this.filePath, `${JSON.stringify(document, null, 2)}
2217
+ `, this.durability);
1057
2218
  }
1058
2219
  /** Serializes an async operation against this file. */
1059
2220
  synchronized(operation) {
@@ -1150,13 +2311,14 @@ var TaskEngine = class {
1150
2311
  createdAt: now,
1151
2312
  updatedAt: now,
1152
2313
  ...input.project !== void 0 ? { project: input.project } : {},
2314
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1153
2315
  ...input.role !== void 0 ? { role: input.role } : {},
1154
2316
  ...input.agent !== void 0 ? { agent: input.agent } : {},
1155
2317
  ...input.context !== void 0 ? { context: input.context } : {}
1156
2318
  };
1157
2319
  await this.options.store.save(task);
1158
2320
  this.log.info(`Created task ${task.id}.`);
1159
- await this.audit("task.created", task.id, { objective: task.objective, project: task.project });
2321
+ await this.audit("task.created", task, { objective: task.objective });
1160
2322
  return task;
1161
2323
  }
1162
2324
  async queue(id) {
@@ -1199,12 +2361,24 @@ var TaskEngine = class {
1199
2361
  const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1200
2362
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1201
2363
  current = await this.transition(current, "running", { attempt });
2364
+ await this.options.audit?.record({
2365
+ at: startedAt,
2366
+ action: "agent.started",
2367
+ id: current.id,
2368
+ taskId: current.id,
2369
+ ...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
2370
+ agentId: task.agent,
2371
+ executionId,
2372
+ detail: { attempt }
2373
+ });
1202
2374
  let output;
1203
2375
  let thrown;
1204
2376
  try {
1205
2377
  output = await agent.run({
1206
2378
  taskId: current.id,
1207
2379
  objective: current.objective,
2380
+ executionId,
2381
+ ...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
1208
2382
  ...role ? { role } : {},
1209
2383
  ...background !== void 0 ? { context: background } : {}
1210
2384
  });
@@ -1212,6 +2386,16 @@ var TaskEngine = class {
1212
2386
  thrown = error;
1213
2387
  }
1214
2388
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2389
+ await this.options.audit?.record({
2390
+ at: finishedAt,
2391
+ action: "agent.completed",
2392
+ id: current.id,
2393
+ taskId: current.id,
2394
+ ...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
2395
+ agentId: task.agent,
2396
+ executionId,
2397
+ detail: { attempt, outcome: thrown !== void 0 || output === void 0 ? "threw" : "returned" }
2398
+ });
1215
2399
  if (thrown !== void 0 || output === void 0) {
1216
2400
  const outcome = classifyThrown(thrown);
1217
2401
  const message = thrown instanceof Error ? thrown.message : String(thrown);
@@ -1236,9 +2420,15 @@ var TaskEngine = class {
1236
2420
  if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
1237
2421
  return current;
1238
2422
  }
2423
+ const currentState = await this.options.store.get(current.id);
2424
+ if (currentState?.status === "cancelled") {
2425
+ current = currentState;
2426
+ break;
2427
+ }
1239
2428
  const result = {
1240
2429
  ...output.summary !== void 0 ? { summary: output.summary } : {},
1241
2430
  ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
2431
+ ...output.protocol !== void 0 ? { protocol: output.protocol } : {},
1242
2432
  // Keep the verbatim output (bounded) for report extraction and audit.
1243
2433
  ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1244
2434
  ...output.error !== void 0 ? { error: output.error } : {},
@@ -1286,7 +2476,7 @@ var TaskEngine = class {
1286
2476
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1287
2477
  };
1288
2478
  await this.options.store.save(reset);
1289
- await this.audit("task.retry", id, { from: task.status });
2479
+ await this.audit("task.retry", reset, { from: task.status });
1290
2480
  this.log.info(`Task ${id} reset for retry.`);
1291
2481
  return reset;
1292
2482
  }
@@ -1319,16 +2509,21 @@ var TaskEngine = class {
1319
2509
  ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
1320
2510
  };
1321
2511
  await this.options.store.save(updated);
1322
- await this.audit(`task.${to === "queued" ? "queued" : to}`, updated.id, {
2512
+ await this.audit(`task.${to === "queued" ? "queued" : to}`, updated, {
1323
2513
  from: task.status
1324
2514
  });
1325
2515
  return updated;
1326
2516
  }
1327
- async audit(action, id, detail) {
2517
+ async audit(action, task, detail, executionId) {
1328
2518
  await this.options.audit?.record({
1329
2519
  at: (/* @__PURE__ */ new Date()).toISOString(),
1330
2520
  action,
1331
- id,
2521
+ id: task.id,
2522
+ taskId: task.id,
2523
+ ...task.project !== void 0 ? { project: task.project } : {},
2524
+ ...task.pipelineId !== void 0 ? { pipelineId: task.pipelineId } : {},
2525
+ ...task.agent !== void 0 ? { agentId: task.agent } : {},
2526
+ ...executionId !== void 0 ? { executionId } : {},
1332
2527
  ...detail !== void 0 ? { detail } : {}
1333
2528
  });
1334
2529
  }
@@ -1347,22 +2542,35 @@ function classifyThrown(error) {
1347
2542
  return "failed";
1348
2543
  }
1349
2544
  function sleep(ms) {
1350
- return new Promise((resolve) => setTimeout(resolve, ms));
2545
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1351
2546
  }
1352
2547
 
1353
2548
  // src/audit/index.ts
1354
- import { appendFile, mkdir as mkdir2 } from "fs/promises";
2549
+ import { appendFile, mkdir as mkdir2, readFile as readFile2, rename as rename2, stat as stat2 } from "fs/promises";
1355
2550
  import { dirname as dirname2 } from "path";
2551
+ var DEFAULT_AUDIT_MAX_BYTES = 5 * 1024 * 1024;
1356
2552
  var FileAuditLog = class {
1357
2553
  filePath;
2554
+ maxBytes;
1358
2555
  log;
1359
- constructor(directory = `${process.cwd()}/.omninode`, log = logger) {
2556
+ constructor(directory = `${process.cwd()}/.omninode`, log = logger, maxBytes = DEFAULT_AUDIT_MAX_BYTES) {
1360
2557
  this.filePath = `${directory}/audit.jsonl`;
2558
+ this.maxBytes = maxBytes;
1361
2559
  this.log = log.child({ component: "audit" });
1362
2560
  }
2561
+ /** Rotates the log once it passes the size budget, keeping one backup. */
2562
+ async rotateIfNeeded() {
2563
+ try {
2564
+ const stats = await stat2(this.filePath);
2565
+ if (stats.size <= this.maxBytes) return;
2566
+ await rename2(this.filePath, `${this.filePath}.1`);
2567
+ } catch {
2568
+ }
2569
+ }
1363
2570
  async record(event) {
1364
2571
  try {
1365
2572
  await mkdir2(dirname2(this.filePath), { recursive: true });
2573
+ await this.rotateIfNeeded();
1366
2574
  await appendFile(this.filePath, `${JSON.stringify(event)}
1367
2575
  `, "utf8");
1368
2576
  } catch (error) {
@@ -1373,7 +2581,6 @@ var FileAuditLog = class {
1373
2581
  }
1374
2582
  /** Recent events, oldest first; used by the CLI. */
1375
2583
  async recent(limit = 20) {
1376
- const { readFile: readFile2 } = await import("fs/promises");
1377
2584
  let raw;
1378
2585
  try {
1379
2586
  raw = await readFile2(this.filePath, "utf8");
@@ -1391,6 +2598,23 @@ var FileAuditLog = class {
1391
2598
  }
1392
2599
  };
1393
2600
 
2601
+ // src/memory/base.ts
2602
+ function defaultSearch(provider, text, options = {}) {
2603
+ return provider.retrieve({
2604
+ text,
2605
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
2606
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
2607
+ });
2608
+ }
2609
+ function defaultMetadata(name, options = {}) {
2610
+ return {
2611
+ name,
2612
+ capabilities: options.capabilities ?? ["retrieve", "store", "search"],
2613
+ relevanceRanking: options.relevanceRanking ?? false,
2614
+ ...options.backend !== void 0 ? { backend: options.backend } : {}
2615
+ };
2616
+ }
2617
+
1394
2618
  // src/memory/local.ts
1395
2619
  var STOPWORDS = /* @__PURE__ */ new Set([
1396
2620
  "the",
@@ -1453,9 +2677,27 @@ var LocalMemoryProvider = class extends JsonFileStore {
1453
2677
  constructor(directory) {
1454
2678
  super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
1455
2679
  }
1456
- async query(query) {
2680
+ metadata() {
2681
+ return defaultMetadata(this.name, {
2682
+ capabilities: ["retrieve", "store", "search", "categories", "tags"],
2683
+ relevanceRanking: true,
2684
+ backend: { kind: "json-file", deterministicScoring: true }
2685
+ });
2686
+ }
2687
+ search(text, options = {}) {
2688
+ return defaultSearch(this, text, options);
2689
+ }
2690
+ /** @deprecated v1 alias of {@link retrieve}. */
2691
+ query(query) {
2692
+ return this.retrieve(query);
2693
+ }
2694
+ /** @deprecated v1 alias of {@link store}. */
2695
+ write(entry) {
2696
+ return this.store(entry);
2697
+ }
2698
+ async retrieve(query) {
1457
2699
  const entries = await this.readAll();
1458
- const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter(
2700
+ const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter((entry) => query.category ? entry.category === query.category : true).filter(
1459
2701
  (entry) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry.tags ?? []).includes(tag)) : true
1460
2702
  );
1461
2703
  if (!query.text || query.text.trim().length === 0) {
@@ -1464,7 +2706,7 @@ var LocalMemoryProvider = class extends JsonFileStore {
1464
2706
  const queryKeywords = keywords(query.text);
1465
2707
  return filtered.map((entry) => ({ entry, score: relevanceScore(queryKeywords, entry) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry }) => entry);
1466
2708
  }
1467
- write(entry) {
2709
+ store(entry) {
1468
2710
  return this.mutate((entries) => {
1469
2711
  const index = entries.findIndex((e) => e.key === entry.key);
1470
2712
  const stamped = {
@@ -1495,11 +2737,40 @@ var RememberaMemoryProvider = class {
1495
2737
  this.env = config.env ?? process.env;
1496
2738
  this.log = log.child({ component: "remembera" });
1497
2739
  }
2740
+ get authenticationConfigured() {
2741
+ return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
2742
+ }
1498
2743
  headers() {
1499
2744
  const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
1500
2745
  return key ? { authorization: `Bearer ${key}` } : {};
1501
2746
  }
1502
- async query(query) {
2747
+ /** Search delegates to the gateway's relevance query. */
2748
+ search(text, options = {}) {
2749
+ return defaultSearch(
2750
+ this,
2751
+ text,
2752
+ {
2753
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
2754
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
2755
+ }
2756
+ );
2757
+ }
2758
+ metadata() {
2759
+ return defaultMetadata(this.name, {
2760
+ capabilities: ["retrieve", "store", "search", "categories", "tags"],
2761
+ relevanceRanking: true,
2762
+ backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
2763
+ });
2764
+ }
2765
+ /** @deprecated v1 alias of {@link retrieve}. */
2766
+ query(query) {
2767
+ return this.retrieve(query);
2768
+ }
2769
+ /** @deprecated v1 alias of {@link store}. */
2770
+ write(entry) {
2771
+ return this.store(entry);
2772
+ }
2773
+ async retrieve(query) {
1503
2774
  const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
1504
2775
  if (status !== 200) {
1505
2776
  throw new MemoryError(`Remembera query returned HTTP ${status}.`);
@@ -1510,7 +2781,7 @@ var RememberaMemoryProvider = class {
1510
2781
  }
1511
2782
  return entries.map((entry) => this.toEntry(entry)).filter((entry) => entry !== void 0);
1512
2783
  }
1513
- async write(entry) {
2784
+ async store(entry) {
1514
2785
  const { status } = await this.request(`${this.baseUrl}/api/memory`, entry);
1515
2786
  if (status !== 200 && status !== 201) {
1516
2787
  throw new MemoryError(`Remembera write returned HTTP ${status}.`);
@@ -1545,6 +2816,7 @@ var RememberaMemoryProvider = class {
1545
2816
  content: entry.content,
1546
2817
  scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
1547
2818
  ...Array.isArray(entry.tags) ? { tags: entry.tags.filter((t) => typeof t === "string") } : {},
2819
+ ...typeof entry.category === "string" ? { category: entry.category } : {},
1548
2820
  ...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
1549
2821
  ...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {}
1550
2822
  };
@@ -1575,57 +2847,54 @@ function createMemoryProvider(config) {
1575
2847
  }
1576
2848
 
1577
2849
  // src/memory/service.ts
2850
+ var DEFAULT_ENTRY_LIMIT = 8;
1578
2851
  var MAX_CONTEXT_CHARS = 4e3;
1579
2852
  var MemoryService = class {
1580
- constructor(provider, log = logger) {
2853
+ constructor(provider, log = logger, options = {}) {
1581
2854
  this.provider = provider;
1582
2855
  this.log = log.child({ component: "memory", provider: provider.name });
2856
+ this.required = options.required ?? false;
2857
+ this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
1583
2858
  }
1584
2859
  provider;
1585
2860
  log;
2861
+ required;
2862
+ maxContextChars;
1586
2863
  get providerRef() {
1587
2864
  return this.provider;
1588
2865
  }
1589
2866
  get providerName() {
1590
2867
  return this.provider.name;
1591
2868
  }
1592
- /** Relevant context for a task objective; empty string when nothing matches. */
1593
- async gatherContext(query) {
2869
+ get isRequired() {
2870
+ return this.required;
2871
+ }
2872
+ /** Structured, budgeted context for a task (§12 — avoid loading all memory). */
2873
+ async contextFor(query) {
2874
+ let entries;
1594
2875
  try {
1595
- const entries = await this.provider.query({
2876
+ entries = await this.provider.retrieve({
1596
2877
  text: query.objective,
1597
2878
  ...query.role ? { tags: [query.role] } : {},
1598
- limit: 8
2879
+ limit: query.limit ?? DEFAULT_ENTRY_LIMIT
1599
2880
  });
1600
- if (entries.length === 0) return "";
1601
- const byScope = /* @__PURE__ */ new Map();
1602
- for (const entry of entries) {
1603
- byScope.set(entry.scope, [...byScope.get(entry.scope) ?? [], entry]);
1604
- }
1605
- const parts = [];
1606
- for (const scope of ["project", "task", "role", "knowledge"]) {
1607
- const scopeEntries = byScope.get(scope) ?? [];
1608
- if (scopeEntries.length === 0) continue;
1609
- const lines = scopeEntries.map((entry) => `- ${entry.content}`).join("\n");
1610
- parts.push(`### Relevant memory (${scope})
1611
- ${lines}`);
1612
- }
1613
- const context = parts.join("\n\n");
1614
- return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
1615
2881
  } catch (error) {
1616
- this.log.warn(
1617
- `Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
1618
- );
1619
- return "";
2882
+ return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
1620
2883
  }
2884
+ return this.format(entries);
2885
+ }
2886
+ /** Prompt-ready context text (v1 convenience wrapper). */
2887
+ async gatherContext(query) {
2888
+ return (await this.contextFor(query)).text;
1621
2889
  }
1622
- /** Records a finished task's outcome — best-effort. */
2890
+ /** Records a finished task's outcome — task history plus "known problems" (§12). */
1623
2891
  async recordTaskOutcome(task) {
1624
2892
  const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
1625
2893
  try {
1626
- await this.provider.write({
2894
+ await this.provider.store({
1627
2895
  key: `task:${task.id}`,
1628
2896
  scope: "task",
2897
+ category: "tasks",
1629
2898
  content: `Objective: ${task.objective}
1630
2899
  Status: ${task.status}
1631
2900
  Outcome: ${outcome}`,
@@ -1636,41 +2905,335 @@ Outcome: ${outcome}`,
1636
2905
  (report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
1637
2906
  );
1638
2907
  for (const { report, finding } of significant) {
1639
- await this.provider.write({
2908
+ await this.provider.store({
1640
2909
  key: `finding:${report.id}:${finding.id}`,
1641
2910
  scope: "project",
2911
+ category: "problems",
1642
2912
  content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
1643
2913
  tags: ["known-problem", report.agent]
1644
2914
  });
1645
2915
  }
1646
2916
  } catch (error) {
1647
- this.log.warn(
1648
- `Memory write failed (continuing): ${error instanceof Error ? error.message : String(error)}`
2917
+ this.handleFailure("store", error, void 0);
2918
+ }
2919
+ }
2920
+ format(entries) {
2921
+ if (entries.length === 0) return { text: "", entries, truncated: false };
2922
+ const byScope = /* @__PURE__ */ new Map();
2923
+ for (const entry of entries) {
2924
+ byScope.set(entry.scope, [...byScope.get(entry.scope) ?? [], entry]);
2925
+ }
2926
+ const parts = [];
2927
+ for (const scope of ["project", "task", "role", "knowledge"]) {
2928
+ const scopeEntries = byScope.get(scope) ?? [];
2929
+ if (scopeEntries.length === 0) continue;
2930
+ const lines = scopeEntries.map((entry) => {
2931
+ const category = entry.category ? `[${entry.category}] ` : "";
2932
+ return `- ${category}${entry.content}`;
2933
+ }).join("\n");
2934
+ parts.push(`### Relevant memory (${scope})
2935
+ ${lines}`);
2936
+ }
2937
+ let text = parts.join("\n\n");
2938
+ const truncated = text.length > this.maxContextChars;
2939
+ if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
2940
+ return { text, entries, truncated };
2941
+ }
2942
+ /**
2943
+ * Failure policy (§12): continue unless memory is explicitly required.
2944
+ */
2945
+ handleFailure(operation, error, fallback) {
2946
+ const message = error instanceof Error ? error.message : String(error);
2947
+ if (this.required) {
2948
+ throw new MemoryError(
2949
+ `Memory is marked required, but ${operation} failed: ${message}`,
2950
+ { cause: error }
1649
2951
  );
1650
2952
  }
2953
+ this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
2954
+ return fallback ?? { text: "", entries: [], truncated: false };
2955
+ }
2956
+ };
2957
+
2958
+ // src/tasks/engine-factory.ts
2959
+ function createTaskEngine(config, log = logger, directory) {
2960
+ const agents = buildAgentRegistry(config, log);
2961
+ const roles = buildRoleRegistry(config);
2962
+ return new TaskEngine({
2963
+ agents,
2964
+ roles,
2965
+ store: new FileTaskStore(directory),
2966
+ ...config.project.memory !== void 0 ? {
2967
+ memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
2968
+ required: config.project.memory.required
2969
+ })
2970
+ } : {},
2971
+ audit: new FileAuditLog(),
2972
+ log
2973
+ });
2974
+ }
2975
+
2976
+ // src/pipelines/engine.ts
2977
+ import { randomBytes as randomBytes5 } from "crypto";
2978
+
2979
+ // src/reports/aggregate.ts
2980
+ import { randomBytes as randomBytes2 } from "crypto";
2981
+
2982
+ // src/reports/schema.ts
2983
+ import { z as z2 } from "zod";
2984
+ var evidenceSchema = z2.union([
2985
+ z2.string().transform((excerpt) => ({ kind: "observation", excerpt })),
2986
+ z2.object({
2987
+ kind: z2.enum(["file", "line", "url", "command", "observation", "artifact"]),
2988
+ ref: z2.string().optional(),
2989
+ line: z2.number().int().optional(),
2990
+ excerpt: z2.string().optional(),
2991
+ agent: z2.string().optional()
2992
+ })
2993
+ ]);
2994
+ var findingSchema = z2.object({
2995
+ id: z2.string().min(1),
2996
+ title: z2.string().min(1),
2997
+ detail: z2.string().default(""),
2998
+ description: z2.string().optional(),
2999
+ severity: z2.enum(["info", "low", "medium", "high", "critical"]).optional(),
3000
+ confidence: z2.enum(["low", "medium", "high"]).optional(),
3001
+ severityBySource: z2.record(z2.enum(["info", "low", "medium", "high", "critical"])).optional(),
3002
+ confidenceBySource: z2.record(z2.enum(["low", "medium", "high"])).optional(),
3003
+ recommendations: z2.array(z2.string()).optional(),
3004
+ evidence: z2.array(evidenceSchema).optional(),
3005
+ location: z2.string().optional(),
3006
+ tags: z2.array(z2.string()).optional(),
3007
+ source: z2.object({ agent: z2.string().optional(), reportId: z2.string().optional(), pipelineRunId: z2.string().optional() }).optional(),
3008
+ recommendation: z2.string().optional()
3009
+ });
3010
+ var artifactSchema = z2.object({
3011
+ kind: z2.enum(["file", "patch", "log", "screenshot", "document", "data"]),
3012
+ ref: z2.string().min(1),
3013
+ description: z2.string().optional(),
3014
+ mediaType: z2.string().optional(),
3015
+ bytes: z2.number().int().optional()
3016
+ });
3017
+ var reportSchema = z2.object({
3018
+ id: z2.string().min(1),
3019
+ taskId: z2.string().min(1),
3020
+ agent: z2.string().min(1),
3021
+ agentId: z2.string().optional(),
3022
+ pipelineRunId: z2.string().optional(),
3023
+ model: z2.string().optional(),
3024
+ summary: z2.string(),
3025
+ findings: z2.array(findingSchema),
3026
+ recommendations: z2.array(z2.string()),
3027
+ confidence: z2.enum(["low", "medium", "high"]).optional(),
3028
+ evidence: z2.array(evidenceSchema).optional(),
3029
+ artifacts: z2.array(artifactSchema).optional(),
3030
+ createdAt: z2.string().min(1),
3031
+ metadata: z2.record(z2.unknown()).optional()
3032
+ });
3033
+ function validateReport(value) {
3034
+ const parsed = reportSchema.safeParse(value);
3035
+ if (!parsed.success) {
3036
+ const issues = parsed.error.issues.map((issue) => ({
3037
+ path: issue.path.join(".") || "(root)",
3038
+ message: issue.message
3039
+ }));
3040
+ return {
3041
+ valid: false,
3042
+ issues,
3043
+ reason: `report failed schema validation (${issues.length} issue(s))`
3044
+ };
3045
+ }
3046
+ const report = parsed.data;
3047
+ report.findings = report.findings.map((finding) => ({
3048
+ ...finding,
3049
+ description: finding.description ?? finding.detail,
3050
+ ...finding.source === void 0 ? { source: { agent: report.agent, reportId: report.id, ...report.pipelineRunId !== void 0 ? { pipelineRunId: report.pipelineRunId } : {} } } : {}
3051
+ }));
3052
+ return { valid: true, report };
3053
+ }
3054
+ function normalizeEvidence(input) {
3055
+ if (input === void 0) return void 0;
3056
+ return input.map(
3057
+ (item) => typeof item === "string" ? { kind: "observation", excerpt: item } : item
3058
+ );
3059
+ }
3060
+ function normalizeFinding(finding) {
3061
+ return {
3062
+ ...finding,
3063
+ description: finding.description ?? finding.detail,
3064
+ evidence: normalizeEvidence(finding.evidence)
3065
+ };
3066
+ }
3067
+
3068
+ // src/reports/aggregate.ts
3069
+ var SEVERITY_RANK = {
3070
+ info: 0,
3071
+ low: 1,
3072
+ medium: 2,
3073
+ high: 3,
3074
+ critical: 4
3075
+ };
3076
+ var MAX_COMBINED_FINDINGS = 200;
3077
+ function aggregateReports(reports, options = {}) {
3078
+ const threshold = options.similarityThreshold ?? 0.7;
3079
+ const findingGroups = [];
3080
+ const recommendationGroups = [];
3081
+ for (const report of reports) {
3082
+ for (const finding of report.findings) {
3083
+ mergeFinding(findingGroups, finding, report, threshold);
3084
+ }
3085
+ for (const text of report.recommendations) {
3086
+ mergeRecommendation(recommendationGroups, text, report, threshold);
3087
+ }
3088
+ }
3089
+ const sources = [...new Set(reports.map((r) => r.agent))].sort();
3090
+ const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
3091
+ const conflicts = detectConflicts(findingGroups);
3092
+ const agreements = findingGroups.filter(
3093
+ (group) => group.sources.length > 1 && group.severity !== void 0 && new Set(Object.values(group.severityBySource ?? {})).size === 1
3094
+ );
3095
+ const maxFindings = options.maxFindings ?? MAX_COMBINED_FINDINGS;
3096
+ const orderedFindings = findingGroups.sort((a, b) => rank(b) - rank(a));
3097
+ const retainedFindings = orderedFindings.slice(0, maxFindings);
3098
+ const droppedFindings = orderedFindings.length - retainedFindings.length;
3099
+ const disputed = conflicts.length;
3100
+ const summary = `${reports.length} report(s) from ${sources.length > 0 ? sources.join(", ") : "no agents"}: ${findingGroups.length} unique finding group(s) (${totalRawFindings} raw), ${agreements.length} agreement(s), ${disputed} conflict(s), ${recommendationGroups.length} recommendation group(s).`;
3101
+ return {
3102
+ id: `combined-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
3103
+ title: options.objective !== void 0 ? truncate(options.objective, 120) : "Combined report",
3104
+ ...options.objective !== void 0 ? { objective: options.objective } : {},
3105
+ ...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
3106
+ taskIds: options.taskIds ?? [],
3107
+ summary,
3108
+ findings: retainedFindings,
3109
+ agreements,
3110
+ conflicts,
3111
+ recommendations: recommendationGroups,
3112
+ sources,
3113
+ reportIds: reports.map((r) => r.id),
3114
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3115
+ metadata: {
3116
+ agents: sources.length,
3117
+ reports: reports.length,
3118
+ rawFindings: totalRawFindings,
3119
+ findingGroups: findingGroups.length,
3120
+ agreements: agreements.length,
3121
+ conflicts: conflicts.length,
3122
+ findingsTruncated: droppedFindings
3123
+ }
3124
+ };
3125
+ }
3126
+ function rankConfidence(confidence) {
3127
+ return confidence ? { low: 1, medium: 2, high: 3 }[confidence] : 0;
3128
+ }
3129
+ function detectConflicts(groups) {
3130
+ const conflicts = [];
3131
+ for (const group of groups) {
3132
+ if (group.sources.length < 2) continue;
3133
+ const severities = new Set(Object.values(group.severityBySource ?? {}));
3134
+ if (severities.size > 1) {
3135
+ conflicts.push({
3136
+ type: "severity",
3137
+ findingTitle: group.title,
3138
+ positions: group.sources.map((source) => position(source, group)),
3139
+ note: `Agents disagree on severity (${[...severities].sort((a, b) => SEVERITY_RANK[a] - SEVERITY_RANK[b]).join(", ")}); the highest severity is shown, every position is preserved.`
3140
+ });
3141
+ }
3142
+ const recommendations = (group.recommendations ?? []).filter((r) => r.length > 0);
3143
+ if (recommendations.length > 1) {
3144
+ conflicts.push({
3145
+ type: "recommendation",
3146
+ findingTitle: group.title,
3147
+ positions: group.sources.map((source) => position(source, group)),
3148
+ note: `Agents proposed ${recommendations.length} different fixes; none was discarded.`
3149
+ });
3150
+ }
3151
+ }
3152
+ return conflicts;
3153
+ }
3154
+ function position(source, group) {
3155
+ return {
3156
+ agent: source.agent,
3157
+ reportId: source.reportId,
3158
+ ...group.severityBySource?.[source.agent] !== void 0 ? { severity: group.severityBySource[source.agent] } : {},
3159
+ ...group.confidenceBySource?.[source.agent] !== void 0 ? { confidence: group.confidenceBySource[source.agent] } : {},
3160
+ ...group.recommendations?.[0] !== void 0 ? { recommendation: group.recommendations[0] } : {}
3161
+ };
3162
+ }
3163
+ function rank(finding) {
3164
+ return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
3165
+ }
3166
+ function mergeFinding(groups, finding, report, threshold) {
3167
+ const source = { agent: report.agent, reportId: report.id };
3168
+ const group = groups.find((g) => similar(g.title, finding.title, threshold));
3169
+ if (group) {
3170
+ group.occurrences += 1;
3171
+ group.details.push(finding.detail);
3172
+ group.sources.push(source);
3173
+ group.evidence.push(...normalizeEvidence(finding.evidence) ?? []);
3174
+ if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
3175
+ group.severity = finding.severity;
3176
+ }
3177
+ if (finding.confidence && rankConfidence(finding.confidence) > rankConfidence(group.confidence)) {
3178
+ group.confidence = finding.confidence;
3179
+ }
3180
+ if (finding.severity) {
3181
+ group.severityBySource = { ...group.severityBySource, [report.agent]: finding.severity };
3182
+ }
3183
+ if (finding.confidence) {
3184
+ group.confidenceBySource = { ...group.confidenceBySource, [report.agent]: finding.confidence };
3185
+ }
3186
+ if (finding.recommendation && !group.recommendations?.includes(finding.recommendation)) {
3187
+ group.recommendations = [...group.recommendations ?? [], finding.recommendation];
3188
+ }
3189
+ return;
1651
3190
  }
1652
- };
1653
-
1654
- // src/tasks/engine-factory.ts
1655
- function createTaskEngine(config, log = logger, directory) {
1656
- const agents = buildAgentRegistry(config, log);
1657
- const roles = buildRoleRegistry(config);
1658
- return new TaskEngine({
1659
- agents,
1660
- roles,
1661
- store: new FileTaskStore(directory),
1662
- ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
1663
- audit: new FileAuditLog(),
1664
- log
3191
+ groups.push({
3192
+ title: finding.title,
3193
+ details: [finding.detail],
3194
+ sources: [source],
3195
+ occurrences: 1,
3196
+ ...finding.severity ? { severity: finding.severity } : {},
3197
+ ...finding.confidence ? { confidence: finding.confidence } : {},
3198
+ ...finding.severity ? { severityBySource: { [report.agent]: finding.severity } } : {},
3199
+ ...finding.confidence ? { confidenceBySource: { [report.agent]: finding.confidence } } : {},
3200
+ ...finding.recommendation ? { recommendations: [finding.recommendation] } : {},
3201
+ evidence: [...normalizeEvidence(finding.evidence) ?? []]
1665
3202
  });
1666
3203
  }
1667
-
1668
- // src/pipelines/engine.ts
1669
- import { randomBytes as randomBytes4 } from "crypto";
3204
+ function mergeRecommendation(groups, text, report, threshold) {
3205
+ const source = { agent: report.agent, reportId: report.id };
3206
+ const group = groups.find((g) => similar(g.text, text, threshold));
3207
+ if (group) {
3208
+ group.occurrences += 1;
3209
+ group.sources.push(source);
3210
+ return;
3211
+ }
3212
+ groups.push({ text, sources: [source], occurrences: 1 });
3213
+ }
3214
+ function similar(a, b, threshold) {
3215
+ const left = tokens(a);
3216
+ const right = tokens(b);
3217
+ if (left.size === 0 || right.size === 0) return false;
3218
+ let intersection = 0;
3219
+ for (const token of left) {
3220
+ if (right.has(token)) intersection += 1;
3221
+ }
3222
+ const union = left.size + right.size - intersection;
3223
+ return intersection / union >= threshold;
3224
+ }
3225
+ function tokens(text) {
3226
+ return new Set(
3227
+ text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
3228
+ );
3229
+ }
3230
+ function truncate(text, max) {
3231
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
3232
+ }
1670
3233
 
1671
3234
  // src/planner/heuristic.ts
1672
- import { randomBytes as randomBytes2 } from "crypto";
1673
- var SEVERITY_RANK = {
3235
+ import { randomBytes as randomBytes3 } from "crypto";
3236
+ var SEVERITY_RANK2 = {
1674
3237
  info: 0,
1675
3238
  low: 1,
1676
3239
  medium: 2,
@@ -1681,19 +3244,32 @@ var HeuristicPlanner = class {
1681
3244
  name = "heuristic";
1682
3245
  async plan(request) {
1683
3246
  const steps = [];
1684
- const findings = request.reports.flatMap(
1685
- (report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
1686
- );
1687
3247
  const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
1688
- for (const [index, text] of recommendations.entries()) {
3248
+ for (const text of recommendations) {
1689
3249
  steps.push({
1690
3250
  id: `step-${steps.length + 1}`,
1691
- title: truncate(text, 120),
3251
+ title: truncate2(text, 120),
1692
3252
  order: steps.length + 1,
1693
- ...index > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
3253
+ ...steps.length > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
1694
3254
  });
1695
3255
  }
1696
- const significant = findings.filter(({ finding }) => finding.severity ? SEVERITY_RANK[finding.severity] >= SEVERITY_RANK.high : false).sort((a, b) => SEVERITY_RANK[b.finding.severity ?? "info"] - SEVERITY_RANK[a.finding.severity ?? "info"]);
3256
+ const contributed = request.aggregated ? request.aggregated.findings.map((finding) => ({
3257
+ finding: {
3258
+ id: `agg-${slug(finding.title)}`,
3259
+ title: finding.title,
3260
+ detail: finding.details[0] ?? finding.title,
3261
+ ...finding.severity ? { severity: finding.severity } : {},
3262
+ ...finding.recommendations && finding.recommendations.length > 0 ? { recommendation: finding.recommendations[0] } : {}
3263
+ },
3264
+ agent: finding.sources.map((s) => s.agent).join("+")
3265
+ })) : request.reports.flatMap(
3266
+ (report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
3267
+ );
3268
+ const significant = contributed.filter(
3269
+ ({ finding }) => finding.severity ? SEVERITY_RANK2[finding.severity] >= SEVERITY_RANK2.high : false
3270
+ ).sort(
3271
+ (a, b) => SEVERITY_RANK2[b.finding.severity ?? "info"] - SEVERITY_RANK2[a.finding.severity ?? "info"]
3272
+ );
1697
3273
  for (const { finding } of significant) {
1698
3274
  steps.push(remediationStep(finding, steps.length + 1));
1699
3275
  }
@@ -1706,12 +3282,20 @@ var HeuristicPlanner = class {
1706
3282
  });
1707
3283
  }
1708
3284
  const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
3285
+ const conflicts = request.aggregated?.conflicts ?? [];
3286
+ if (conflicts.length > 0) {
3287
+ risks.push(
3288
+ `${conflicts.length} unresolved agent disagreement(s): ${conflicts.map((c) => c.findingTitle).join("; ")}`
3289
+ );
3290
+ }
1709
3291
  return {
1710
- id: `plan-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
3292
+ id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
1711
3293
  objective: request.objective,
3294
+ goal: request.aggregated?.title ?? truncate2(request.objective, 120),
1712
3295
  summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
1713
3296
  steps,
1714
3297
  ...risks.length > 0 ? { risks } : {},
3298
+ ...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
1715
3299
  generatedBy: "heuristic",
1716
3300
  ...request.taskId !== void 0 ? { taskId: request.taskId } : {},
1717
3301
  ...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
@@ -1722,28 +3306,80 @@ var HeuristicPlanner = class {
1722
3306
  function remediationStep(finding, order) {
1723
3307
  return {
1724
3308
  id: `step-${order}`,
1725
- title: `Fix: ${truncate(finding.title, 100)}`,
3309
+ title: `Fix: ${truncate2(finding.title, 100)}`,
1726
3310
  description: finding.detail,
1727
3311
  order,
1728
3312
  dependsOn: order > 1 ? [`step-${order - 1}`] : [],
1729
- sourceFindings: [finding.id]
3313
+ sourceFindings: [finding.id],
3314
+ ...finding.recommendation ? { recommendation: finding.recommendation } : {}
1730
3315
  };
1731
3316
  }
1732
3317
  function dedupe(texts) {
1733
3318
  return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
1734
3319
  }
1735
- function truncate(text, max) {
3320
+ function slug(text) {
3321
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
3322
+ }
3323
+ function truncate2(text, max) {
1736
3324
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1737
3325
  }
1738
3326
 
1739
3327
  // src/planner/model.ts
1740
- import { randomBytes as randomBytes3 } from "crypto";
1741
- import { z as z2 } from "zod";
3328
+ import { randomBytes as randomBytes4 } from "crypto";
3329
+
3330
+ // src/planner/schema.ts
3331
+ import { z as z3 } from "zod";
3332
+ var planStepSchema = z3.object({
3333
+ id: z3.string().min(1).optional(),
3334
+ title: z3.string().min(1),
3335
+ description: z3.string().optional(),
3336
+ order: z3.number().int().optional(),
3337
+ targets: z3.array(z3.string()).optional(),
3338
+ acceptance_criteria: z3.array(z3.string()).optional(),
3339
+ depends_on: z3.array(z3.string()).optional(),
3340
+ source_findings: z3.array(z3.string()).optional()
3341
+ });
3342
+ var planJsonSchema = z3.object({
3343
+ goal: z3.string().min(1),
3344
+ summary: z3.string().min(1),
3345
+ steps: z3.array(planStepSchema).min(1),
3346
+ risks: z3.array(z3.string()).optional(),
3347
+ notes: z3.array(z3.string()).optional()
3348
+ });
3349
+ function validatePlan(plan) {
3350
+ const issues = [];
3351
+ const stepIds = /* @__PURE__ */ new Set();
3352
+ for (const [index, step] of plan.steps.entries()) {
3353
+ if (stepIds.has(step.id)) {
3354
+ issues.push({ path: `steps.${index}.id`, message: `duplicate step id "${step.id}"` });
3355
+ }
3356
+ stepIds.add(step.id);
3357
+ if (!step.title || step.title.trim().length === 0) {
3358
+ issues.push({ path: `steps.${index}.title`, message: "title must not be empty" });
3359
+ }
3360
+ for (const dep of step.dependsOn ?? []) {
3361
+ if (!stepIds.has(dep) && !plan.steps.some((s) => s.id === dep)) {
3362
+ issues.push({ path: `steps.${index}.dependsOn`, message: `unknown step dependency "${dep}"` });
3363
+ }
3364
+ }
3365
+ }
3366
+ if (plan.steps.length === 0) {
3367
+ issues.push({ path: "steps", message: "a plan needs at least one step" });
3368
+ }
3369
+ if (!plan.summary || plan.summary.trim().length === 0) {
3370
+ issues.push({ path: "summary", message: "summary must not be empty" });
3371
+ }
3372
+ return issues.length > 0 ? { valid: false, issues, reason: `plan failed validation (${issues.length} issue(s))` } : { valid: true, plan };
3373
+ }
1742
3374
 
1743
3375
  // src/planner/context.ts
1744
3376
  function buildPlannerContext(request) {
1745
- const parts = [`# Objective
3377
+ const parts = [`# Task
1746
3378
  ${request.objective}`];
3379
+ if (request.projectContext) {
3380
+ parts.push(`# Project context
3381
+ ${request.projectContext}`);
3382
+ }
1747
3383
  if (request.role) {
1748
3384
  const roleLines = [`# Role
1749
3385
  ${request.role.name} (${request.role.id})`];
@@ -1763,7 +3399,7 @@ ${request.role.name} (${request.role.id})`];
1763
3399
  )
1764
3400
  );
1765
3401
  parts.push(
1766
- `# Findings (${request.reports.length} report(s) by ${agents})
3402
+ `# Agent reports (${request.reports.length} by ${agents})
1767
3403
  ` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
1768
3404
  );
1769
3405
  const recommendationLines = request.reports.flatMap(
@@ -1773,9 +3409,20 @@ ${request.role.name} (${request.role.id})`];
1773
3409
  parts.push(`# Recommendations
1774
3410
  ${recommendationLines.join("\n")}`);
1775
3411
  }
1776
- const summaryLines = request.reports.map((report) => `- [${report.agent}] ${report.summary}`);
1777
- parts.push(`# Report summaries
1778
- ${summaryLines.join("\n")}`);
3412
+ }
3413
+ if (request.aggregated) {
3414
+ const aggregatedLines = [
3415
+ `- findings: ${request.aggregated.findings.length} (agreements: ${request.aggregated.agreements.length}, conflicts: ${request.aggregated.conflicts.length})`,
3416
+ ...request.aggregated.findings.map(
3417
+ (finding) => `- [${finding.severity ?? "info"}] ${finding.title} (x${finding.occurrences})`
3418
+ ),
3419
+ // Conflicts stay visible to the planner — it must plan around disagreement.
3420
+ ...request.aggregated.conflicts.map(
3421
+ (conflict) => `- CONFLICT (${conflict.type}) on "${conflict.findingTitle}": ${conflict.note}`
3422
+ )
3423
+ ];
3424
+ parts.push(`# Aggregated findings
3425
+ ${aggregatedLines.join("\n")}`);
1779
3426
  }
1780
3427
  if (request.memory && request.memory.length > 0) {
1781
3428
  parts.push(
@@ -1783,7 +3430,11 @@ ${summaryLines.join("\n")}`);
1783
3430
  ` + request.memory.map((entry) => `- [${entry.scope}] ${entry.content}`).join("\n")
1784
3431
  );
1785
3432
  }
1786
- if (request.context && request.context.length > 0) {
3433
+ if (request.constraints && request.constraints.length > 0) {
3434
+ parts.push(`# Constraints
3435
+ ${request.constraints.map((c) => `- ${c}`).join("\n")}`);
3436
+ }
3437
+ if (request.context) {
1787
3438
  parts.push(`# Research context
1788
3439
  ${request.context}`);
1789
3440
  }
@@ -1791,22 +3442,6 @@ ${request.context}`);
1791
3442
  }
1792
3443
 
1793
3444
  // src/planner/model.ts
1794
- var planStepJsonSchema = z2.object({
1795
- id: z2.string().optional(),
1796
- title: z2.string().min(1),
1797
- description: z2.string().optional(),
1798
- order: z2.number().int().optional(),
1799
- targets: z2.array(z2.string()).optional(),
1800
- acceptance_criteria: z2.array(z2.string()).optional(),
1801
- depends_on: z2.array(z2.string()).optional(),
1802
- source_findings: z2.array(z2.string()).optional()
1803
- }).strict();
1804
- var planJsonSchema = z2.object({
1805
- summary: z2.string().min(1),
1806
- steps: z2.array(planStepJsonSchema).min(1),
1807
- risks: z2.array(z2.string()).optional(),
1808
- notes: z2.array(z2.string()).optional()
1809
- }).strict();
1810
3445
  var ModelPlanner = class {
1811
3446
  constructor(options) {
1812
3447
  this.options = options;
@@ -1820,7 +3455,7 @@ var ModelPlanner = class {
1820
3455
  const messages = [
1821
3456
  {
1822
3457
  role: "system",
1823
- content: 'You are the OmniNode planner. Given the objective and the combined research context, produce a final analysis and a prioritized, actionable implementation plan. Respond with JSON ONLY (no prose, no code fences) matching exactly this shape: {"summary": string, "steps": [{"id": string, "title": string, "description": string, "order": number, "targets": string[], "acceptance_criteria": string[], "source_findings": string[]}], "risks": string[], "notes": string[]}. ' + (this.options.instruction ?? "")
3458
+ content: 'You are the OmniNode planner. Given the objective and the combined research context, produce a final analysis and a prioritized, actionable implementation plan. Respond with JSON ONLY (no prose, no code fences) matching exactly this shape: {"goal": string, "summary": string, "steps": [{"id": string, "title": string, "description": string, "order": number, "targets": string[], "acceptance_criteria": string[], "source_findings": string[]}], "risks": string[], "notes": string[]}. ' + (this.options.instruction ?? "")
1824
3459
  },
1825
3460
  { role: "user", content: context }
1826
3461
  ];
@@ -1831,66 +3466,99 @@ var ModelPlanner = class {
1831
3466
  return this.degrade(request, `planner model call failed: ${describe(error)}`);
1832
3467
  }
1833
3468
  const parsed = parsePlanJson(responseText);
1834
- if (!parsed) {
1835
- return this.degrade(request, "planner model output was not valid plan JSON");
3469
+ if (!parsed.ok) {
3470
+ return this.degrade(request, `planner model output rejected: ${parsed.reason}`, parsed.issues);
1836
3471
  }
1837
3472
  const plan = {
1838
- id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
3473
+ id: `plan-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
1839
3474
  objective: request.objective,
3475
+ goal: parsed.goal,
1840
3476
  summary: parsed.summary,
1841
3477
  steps: parsed.steps,
1842
3478
  ...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
1843
3479
  ...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
3480
+ ...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
1844
3481
  generatedBy: `model:${this.options.model}`,
1845
3482
  model: this.options.model,
1846
3483
  ...request.taskId !== void 0 ? { taskId: request.taskId } : {},
1847
3484
  ...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
1848
3485
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1849
3486
  };
1850
- return plan;
3487
+ const validation = validatePlan(plan);
3488
+ if (!validation.valid) {
3489
+ return this.degrade(request, `generated plan rejected: ${validation.reason}`, validation.issues);
3490
+ }
3491
+ return validation.plan;
1851
3492
  }
1852
- async degrade(request, reason) {
3493
+ async degrade(request, reason, issues = []) {
1853
3494
  if (this.options.fallback) {
1854
3495
  this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
1855
3496
  return this.options.fallback.plan(request);
1856
3497
  }
1857
- throw new PlannerError("PLANNER_FAILED", `Planner failed: ${reason}.`);
3498
+ throw new PlannerError("PLAN_INVALID", `Planner failed: ${reason}.`, {
3499
+ details: { reason, issues }
3500
+ });
1858
3501
  }
1859
3502
  };
1860
3503
  function parsePlanJson(text) {
1861
3504
  const unfenced = text.replace(/```(?:json)?/gi, "");
1862
3505
  const start = unfenced.indexOf("{");
1863
3506
  const end = unfenced.lastIndexOf("}");
1864
- if (start < 0 || end <= start) return void 0;
3507
+ if (start < 0 || end <= start) {
3508
+ return { ok: false, reason: "planner output contained no JSON object", issues: [] };
3509
+ }
3510
+ let json;
1865
3511
  try {
1866
- const json = JSON.parse(unfenced.slice(start, end + 1));
1867
- const parsed = planJsonSchema.safeParse(json);
1868
- if (!parsed.success) return void 0;
1869
- const steps = parsed.data.steps.map((step, index) => ({
1870
- id: step.id ?? `step-${index + 1}`,
1871
- title: step.title,
1872
- description: step.description,
1873
- order: step.order ?? index + 1,
1874
- targets: step.targets,
1875
- acceptanceCriteria: step.acceptance_criteria,
1876
- dependsOn: step.depends_on,
1877
- sourceFindings: step.source_findings
1878
- }));
3512
+ json = JSON.parse(unfenced.slice(start, end + 1));
3513
+ } catch (error) {
1879
3514
  return {
1880
- summary: parsed.data.summary,
1881
- steps: steps.sort((a, b) => a.order - b.order),
1882
- risks: parsed.data.risks,
1883
- notes: parsed.data.notes
3515
+ ok: false,
3516
+ reason: `planner output was not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
3517
+ issues: []
3518
+ };
3519
+ }
3520
+ const parsed = planJsonSchema.safeParse(json);
3521
+ if (!parsed.success) {
3522
+ return {
3523
+ ok: false,
3524
+ reason: "planner output did not match the plan schema",
3525
+ issues: parsed.error.issues.map((issue) => ({
3526
+ path: issue.path.join(".") || "(root)",
3527
+ message: issue.message
3528
+ }))
1884
3529
  };
1885
- } catch {
1886
- return void 0;
1887
3530
  }
3531
+ const steps = parsed.data.steps.map((step, index) => ({
3532
+ id: step.id ?? `step-${index + 1}`,
3533
+ title: step.title,
3534
+ description: step.description,
3535
+ order: step.order ?? index + 1,
3536
+ targets: step.targets,
3537
+ acceptanceCriteria: step.acceptance_criteria,
3538
+ dependsOn: step.depends_on,
3539
+ sourceFindings: step.source_findings
3540
+ })).sort((a, b) => a.order - b.order);
3541
+ return {
3542
+ ok: true,
3543
+ goal: parsed.data.goal,
3544
+ summary: parsed.data.summary,
3545
+ steps,
3546
+ risks: parsed.data.risks,
3547
+ notes: parsed.data.notes
3548
+ };
1888
3549
  }
1889
3550
  function describe(error) {
1890
3551
  return error instanceof Error ? error.message : String(error);
1891
3552
  }
1892
3553
 
1893
3554
  // src/pipelines/engine.ts
3555
+ var DEFAULT_MAX_PARALLEL_STEPS = 8;
3556
+ var MAX_COMBINED_CONTEXT_CHARS = 2e4;
3557
+ function budgetContext(context, maxChars = MAX_COMBINED_CONTEXT_CHARS) {
3558
+ if (context.length <= maxChars) return context;
3559
+ const marker = "\n[earlier context trimmed]\n";
3560
+ return marker + context.slice(context.length - (maxChars - marker.length));
3561
+ }
1894
3562
  var PipelineEngine = class {
1895
3563
  constructor(options) {
1896
3564
  this.options = options;
@@ -1898,9 +3566,23 @@ var PipelineEngine = class {
1898
3566
  }
1899
3567
  options;
1900
3568
  log;
3569
+ /** Runs executing in THIS process — cancellation can terminate them directly. */
3570
+ activeRuns = /* @__PURE__ */ new Map();
1901
3571
  get store() {
1902
3572
  return this.options.store;
1903
3573
  }
3574
+ /**
3575
+ * Saves the run while preserving fields that may be set by another process
3576
+ * (e.g. a cancellation request) — an in-memory snapshot must never clobber
3577
+ * externally requested cancellation (§9.2).
3578
+ */
3579
+ async persist(run) {
3580
+ const current = await this.options.store.get(run.id);
3581
+ await this.options.store.save({
3582
+ ...run,
3583
+ ...current?.cancellationRequested === true ? { cancellationRequested: true, cancellationRequestedAt: current.cancellationRequestedAt } : {}
3584
+ });
3585
+ }
1904
3586
  /** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
1905
3587
  validate(def) {
1906
3588
  const ids = /* @__PURE__ */ new Set();
@@ -2000,22 +3682,49 @@ var PipelineEngine = class {
2000
3682
  );
2001
3683
  }
2002
3684
  }
3685
+ if (step.model) {
3686
+ const separator = step.model.indexOf(":");
3687
+ if (separator <= 0 || separator === step.model.length - 1) {
3688
+ throw new PipelineError(
3689
+ "PIPELINE_INVALID",
3690
+ `Step "${step.id}" has an invalid model reference "${step.model}" \u2014 use "provider:model-id".`
3691
+ );
3692
+ }
3693
+ const providerName = step.model.slice(0, separator);
3694
+ if (!this.options.providers.some((p) => p.name === providerName)) {
3695
+ throw new PipelineError(
3696
+ "PIPELINE_INVALID",
3697
+ `Step "${step.id}" uses model "${step.model}" but provider "${providerName}" is not configured.`
3698
+ );
3699
+ }
3700
+ }
2003
3701
  }
2004
3702
  const now = (/* @__PURE__ */ new Date()).toISOString();
2005
3703
  const run = {
2006
- id: `run-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
3704
+ id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
2007
3705
  pipelineId: def.id,
3706
+ attempt: 1,
2008
3707
  objective,
2009
3708
  status: "running",
2010
3709
  startedAt: now,
2011
3710
  stepRuns: []
2012
3711
  };
2013
3712
  await this.options.store.save(run);
2014
- this.log.info(`Running pipeline ${def.id} as ${run.id}.`);
3713
+ const liveTaskIds = /* @__PURE__ */ new Set();
3714
+ this.activeRuns.set(run.id, {
3715
+ taskIds: liveTaskIds,
3716
+ cancelTasks: async () => {
3717
+ for (const taskId of liveTaskIds) {
3718
+ await this.options.tasks.cancel(taskId).catch(() => void 0);
3719
+ }
3720
+ }
3721
+ });
3722
+ this.log.info(`Running pipeline ${def.id} as ${run.id} (attempt ${run.attempt}).`);
2015
3723
  await this.options.audit?.record({
2016
3724
  at: (/* @__PURE__ */ new Date()).toISOString(),
2017
3725
  action: "pipeline.run.started",
2018
3726
  id: run.id,
3727
+ pipelineId: def.id,
2019
3728
  detail: { pipeline: def.id, objective }
2020
3729
  });
2021
3730
  const deps = this.effectiveDependencies(def);
@@ -2023,28 +3732,57 @@ var PipelineEngine = class {
2023
3732
  const pending = new Set(def.steps.map((s) => s.id));
2024
3733
  const contextParts = [];
2025
3734
  const allReports = [];
3735
+ let cancelled = false;
2026
3736
  while (pending.size > 0) {
3737
+ const persisted = await this.options.store.get(run.id);
3738
+ if (persisted?.cancellationRequested === true) {
3739
+ cancelled = true;
3740
+ run.cancellationRequested = true;
3741
+ run.cancellationRequestedAt = persisted.cancellationRequestedAt;
3742
+ await this.activeRuns.get(run.id)?.cancelTasks();
3743
+ for (const step of def.steps.filter((s) => pending.has(s.id))) {
3744
+ const stepRun = { stepId: step.id, status: "cancelled" };
3745
+ run.stepRuns.push(stepRun);
3746
+ options.onStep?.(stepRun);
3747
+ }
3748
+ pending.clear();
3749
+ break;
3750
+ }
2027
3751
  const ready = def.steps.filter(
2028
3752
  (step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
2029
3753
  );
2030
3754
  if (ready.length === 0) {
2031
3755
  throw new PipelineError("PIPELINE_FAILED", `Pipeline "${def.id}" scheduling stalled.`);
2032
3756
  }
2033
- const settled = await Promise.all(
2034
- ready.map(async (step) => {
2035
- if (!this.shouldRun(step, deps.get(step.id) ?? [], outcomes)) {
2036
- return { status: "skipped", summary: "", taskIds: [], reports: [] };
2037
- }
2038
- const ctx = {
2039
- objective,
2040
- ...role ? { role } : {},
2041
- combinedContext: contextParts.join("\n\n"),
2042
- reports: [...allReports],
2043
- pipelineRunId: run.id
2044
- };
2045
- return this.executeStep(step, ctx);
2046
- })
2047
- );
3757
+ const settled = [];
3758
+ const limit = Math.max(1, this.options.maxParallelSteps ?? DEFAULT_MAX_PARALLEL_STEPS);
3759
+ for (let offset = 0; offset < ready.length; offset += limit) {
3760
+ const batch = ready.slice(offset, offset + limit);
3761
+ settled.push(
3762
+ ...await Promise.all(
3763
+ batch.map(async (step) => {
3764
+ if (!this.shouldRun(step, deps.get(step.id) ?? [], outcomes)) {
3765
+ return { status: "skipped", summary: "", taskIds: [], reports: [] };
3766
+ }
3767
+ const ctx = {
3768
+ maxContextChars: this.options.maxContextChars,
3769
+ objective,
3770
+ ...role ? { role } : {},
3771
+ combinedContext: budgetContext(
3772
+ contextParts.join("\n\n"),
3773
+ this.options.maxContextChars ?? MAX_COMBINED_CONTEXT_CHARS
3774
+ ),
3775
+ reports: [...allReports],
3776
+ pipelineRunId: run.id,
3777
+ runId: run.id,
3778
+ pipelineDefId: def.id,
3779
+ taskId: run.id
3780
+ };
3781
+ return this.executeStep(step, ctx);
3782
+ })
3783
+ )
3784
+ );
3785
+ }
2048
3786
  ready.forEach((step, index) => {
2049
3787
  const outcome = settled[index];
2050
3788
  outcomes.set(step.id, outcome);
@@ -2057,6 +3795,8 @@ ${contribution}`);
2057
3795
  }
2058
3796
  const stepRun = {
2059
3797
  stepId: step.id,
3798
+ attempt: outcome.attempt ?? 1,
3799
+ ...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
2060
3800
  status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
2061
3801
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
2062
3802
  ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
@@ -2066,12 +3806,12 @@ ${contribution}`);
2066
3806
  options.onStep?.(stepRun);
2067
3807
  });
2068
3808
  run.reports = [...allReports];
2069
- await this.options.store.save(run);
3809
+ await this.persist(run);
2070
3810
  }
2071
3811
  const allOutcomes = [...outcomes.values()];
2072
- const failed = allOutcomes.some((o) => o.status === "failed");
2073
- const partial = !failed && allOutcomes.some((o) => o.status === "partial");
2074
- const finalStatus = failed ? "failed" : partial ? "partial" : "completed";
3812
+ const failed = !cancelled && allOutcomes.some((o) => o.status === "failed");
3813
+ const partial = !cancelled && !failed && allOutcomes.some((o) => o.status === "partial");
3814
+ const finalStatus = cancelled ? "cancelled" : failed ? "failed" : partial ? "partial" : "completed";
2075
3815
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
2076
3816
  let resultSummary;
2077
3817
  for (let i = def.steps.length - 1; i >= 0; i -= 1) {
@@ -2099,25 +3839,46 @@ ${contribution}`);
2099
3839
  run.reports = collected;
2100
3840
  }
2101
3841
  }
3842
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
3843
+ this.activeRuns.delete(run.id);
2102
3844
  const finished = {
2103
3845
  ...run,
2104
3846
  status: finalStatus,
2105
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3847
+ finishedAt,
3848
+ attempts: [...run.attempts ?? [], { attempt: run.attempt, startedAt: run.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
2106
3849
  ...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
2107
3850
  ...combinedReportId !== void 0 ? { combinedReportId } : {},
2108
3851
  ...planId !== void 0 ? { planId } : {},
2109
3852
  ...resultSummary !== void 0 ? { resultSummary } : {}
2110
3853
  };
2111
- await this.options.store.save(finished);
3854
+ await this.persist(finished);
2112
3855
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
2113
3856
  await this.options.audit?.record({
2114
3857
  at: (/* @__PURE__ */ new Date()).toISOString(),
2115
- action: "pipeline.run.finished",
3858
+ action: cancelled ? "pipeline.run.cancelled" : finalStatus === "failed" ? "pipeline.run.failed" : "pipeline.run.finished",
2116
3859
  id: run.id,
3860
+ pipelineId: def.id,
2117
3861
  detail: { pipeline: def.id, status: finalStatus }
2118
3862
  });
2119
3863
  return finished;
2120
3864
  }
3865
+ /** First-class cancellation (§9.2): requests cancellation and terminates
3866
+ * tasks that are running in this process immediately. */
3867
+ async cancel(runId) {
3868
+ const run = await this.options.store.get(runId);
3869
+ if (!run) {
3870
+ throw new PipelineError("PIPELINE_NOT_FOUND", `Pipeline run "${runId}" does not exist.`);
3871
+ }
3872
+ await this.options.store.save({
3873
+ ...run,
3874
+ cancellationRequested: true,
3875
+ cancellationRequestedAt: (/* @__PURE__ */ new Date()).toISOString()
3876
+ });
3877
+ const active = this.activeRuns.get(runId);
3878
+ if (active) {
3879
+ await active.cancelTasks();
3880
+ }
3881
+ }
2121
3882
  async get(runId) {
2122
3883
  return this.options.store.get(runId);
2123
3884
  }
@@ -2141,9 +3902,16 @@ ${contribution}`);
2141
3902
  this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
2142
3903
  }
2143
3904
  last = await this.executeStepOnce(step, ctx);
2144
- if (last.status !== "failed") return last;
3905
+ if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
2145
3906
  }
2146
- return last;
3907
+ return { ...last, attempt: retries + 1 };
3908
+ }
3909
+ /** Registers tasks created for a step as live, so cancellation can terminate them (§9.2). */
3910
+ trackLiveTasks(runId, taskIds) {
3911
+ if (!runId) return;
3912
+ const active = this.activeRuns.get(runId);
3913
+ if (!active) return;
3914
+ for (const id of taskIds) active.taskIds.add(id);
2147
3915
  }
2148
3916
  async executeStepOnce(step, ctx) {
2149
3917
  switch (step.kind) {
@@ -2170,13 +3938,16 @@ ${contribution}`);
2170
3938
  agentNames.map(
2171
3939
  (name) => this.options.tasks.create({
2172
3940
  objective: ctx.objective,
3941
+ ...ctx.pipelineRunId !== void 0 ? { pipelineId: ctx.pipelineRunId } : {},
2173
3942
  agent: name,
2174
3943
  ...ctx.role ? { role: ctx.role.id } : {},
2175
3944
  ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
2176
3945
  })
2177
3946
  )
2178
3947
  );
3948
+ this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
2179
3949
  const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
3950
+ const executionIds = results.map((r) => r.executions?.at(-1)?.executionId).filter((id) => id !== void 0);
2180
3951
  const completedTasks = results.filter((r) => r.status === "completed");
2181
3952
  const failedTasks = results.filter((r) => r.status !== "completed");
2182
3953
  const lines = results.map((result, index) => {
@@ -2199,15 +3970,23 @@ ${contribution}`);
2199
3970
  reports
2200
3971
  };
2201
3972
  }
2202
- return { status: "completed", summary, taskIds: created.map((t) => t.id), reports };
3973
+ return {
3974
+ status: "completed",
3975
+ summary,
3976
+ taskIds: created.map((t) => t.id),
3977
+ ...executionIds.length > 0 ? { executionIds } : {},
3978
+ reports
3979
+ };
2203
3980
  }
2204
3981
  async runSingle(step, ctx, agentName) {
2205
3982
  const task = await this.options.tasks.create({
2206
3983
  objective: ctx.objective,
3984
+ ...ctx.pipelineRunId !== void 0 ? { pipelineId: ctx.pipelineRunId } : {},
2207
3985
  agent: agentName,
2208
3986
  ...ctx.role ? { role: ctx.role.id } : {},
2209
3987
  ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
2210
3988
  });
3989
+ this.trackLiveTasks(ctx.runId, [task.id]);
2211
3990
  const result = await this.options.tasks.run(task.id);
2212
3991
  if (result.status !== "completed") {
2213
3992
  return {
@@ -2228,11 +4007,16 @@ ${contribution}`);
2228
4007
  };
2229
4008
  }
2230
4009
  async runPlan(step, ctx) {
4010
+ const aggregated = ctx.reports.length > 0 ? aggregateReports(ctx.reports, {
4011
+ ...step.constraints !== void 0 && step.constraints.length > 0 ? { maxFindings: MAX_COMBINED_FINDINGS } : {}
4012
+ }) : void 0;
2231
4013
  const request = {
2232
4014
  objective: ctx.objective,
2233
4015
  reports: ctx.reports,
4016
+ ...aggregated !== void 0 ? { aggregated } : {},
2234
4017
  ...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
2235
4018
  ...ctx.role ? { role: ctx.role } : {},
4019
+ ...step.constraints !== void 0 && step.constraints.length > 0 ? { constraints: step.constraints } : {},
2236
4020
  ...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
2237
4021
  };
2238
4022
  let planner;
@@ -2270,7 +4054,13 @@ ${contribution}`);
2270
4054
  at: (/* @__PURE__ */ new Date()).toISOString(),
2271
4055
  action: "plan.generated",
2272
4056
  id: plan.id,
2273
- detail: { generatedBy: plan.generatedBy, steps: plan.steps.length }
4057
+ ...ctx.pipelineDefId !== void 0 ? { pipelineId: ctx.pipelineDefId } : {},
4058
+ detail: {
4059
+ generatedBy: plan.generatedBy,
4060
+ steps: plan.steps.length,
4061
+ run: ctx.pipelineRunId,
4062
+ taskId: ctx.taskId
4063
+ }
2274
4064
  });
2275
4065
  return {
2276
4066
  status: "completed",
@@ -2304,7 +4094,7 @@ function formatPlan(plan) {
2304
4094
  }
2305
4095
  return lines.join("\n");
2306
4096
  }
2307
- function createDefaultChatFn(providers) {
4097
+ function createDefaultChatFn(providers, options = {}) {
2308
4098
  return async (modelRef, messages) => {
2309
4099
  const separator = modelRef.indexOf(":");
2310
4100
  if (separator <= 0 || separator === modelRef.length - 1) {
@@ -2322,7 +4112,10 @@ function createDefaultChatFn(providers) {
2322
4112
  `Provider "${providerName}" (model "${modelRef}") is not configured.`
2323
4113
  );
2324
4114
  }
2325
- const provider = createProvider(providerConfig);
4115
+ const provider = createProvider(providerConfig, {
4116
+ ...options.audit !== void 0 ? { audit: options.audit } : {},
4117
+ ...options.env !== void 0 ? { env: options.env } : {}
4118
+ });
2326
4119
  const response = await provider.chat({ model: modelId, messages });
2327
4120
  return response.content;
2328
4121
  };
@@ -2391,99 +4184,6 @@ function buildPlanner(plannerConfig, chat, log = logger) {
2391
4184
  });
2392
4185
  }
2393
4186
 
2394
- // src/reports/aggregate.ts
2395
- import { randomBytes as randomBytes5 } from "crypto";
2396
- var SEVERITY_RANK2 = {
2397
- info: 0,
2398
- low: 1,
2399
- medium: 2,
2400
- high: 3,
2401
- critical: 4
2402
- };
2403
- function aggregateReports(reports, options = {}) {
2404
- const threshold = options.similarityThreshold ?? 0.7;
2405
- const findingGroups = [];
2406
- const recommendationGroups = [];
2407
- for (const report of reports) {
2408
- for (const finding of report.findings) {
2409
- mergeFinding(findingGroups, finding, report, threshold);
2410
- }
2411
- for (const text of report.recommendations) {
2412
- mergeRecommendation(recommendationGroups, text, report, threshold);
2413
- }
2414
- }
2415
- const sources = [...new Set(reports.map((r) => r.agent))].sort();
2416
- const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
2417
- const summary = `${reports.length} report(s) from ${sources.length > 0 ? sources.join(", ") : "no agents"}: ${findingGroups.length} unique finding group(s) (${totalRawFindings} raw), ${recommendationGroups.length} recommendation group(s).`;
2418
- return {
2419
- id: `combined-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
2420
- title: options.objective !== void 0 ? truncate2(options.objective, 120) : "Combined report",
2421
- ...options.objective !== void 0 ? { objective: options.objective } : {},
2422
- ...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
2423
- taskIds: options.taskIds ?? [],
2424
- summary,
2425
- findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
2426
- recommendations: recommendationGroups,
2427
- sources,
2428
- reportIds: reports.map((r) => r.id),
2429
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
2430
- };
2431
- }
2432
- function rank(finding) {
2433
- return finding.severity ? SEVERITY_RANK2[finding.severity] : -1;
2434
- }
2435
- function mergeFinding(groups, finding, report, threshold) {
2436
- const source = { agent: report.agent, reportId: report.id };
2437
- const group = groups.find((g) => similar(g.title, finding.title, threshold));
2438
- if (group) {
2439
- group.occurrences += 1;
2440
- group.details.push(finding.detail);
2441
- group.sources.push(source);
2442
- group.evidence.push(...finding.evidence ?? []);
2443
- if (finding.severity && (!group.severity || SEVERITY_RANK2[finding.severity] > SEVERITY_RANK2[group.severity])) {
2444
- group.severity = finding.severity;
2445
- }
2446
- return;
2447
- }
2448
- groups.push({
2449
- title: finding.title,
2450
- details: [finding.detail],
2451
- sources: [source],
2452
- occurrences: 1,
2453
- ...finding.severity ? { severity: finding.severity } : {},
2454
- evidence: [...finding.evidence ?? []]
2455
- });
2456
- }
2457
- function mergeRecommendation(groups, text, report, threshold) {
2458
- const source = { agent: report.agent, reportId: report.id };
2459
- const group = groups.find((g) => similar(g.text, text, threshold));
2460
- if (group) {
2461
- group.occurrences += 1;
2462
- group.sources.push(source);
2463
- return;
2464
- }
2465
- groups.push({ text, sources: [source], occurrences: 1 });
2466
- }
2467
- function similar(a, b, threshold) {
2468
- const left = tokens(a);
2469
- const right = tokens(b);
2470
- if (left.size === 0 || right.size === 0) return false;
2471
- let intersection = 0;
2472
- for (const token of left) {
2473
- if (right.has(token)) intersection += 1;
2474
- }
2475
- const union = left.size + right.size - intersection;
2476
- return intersection / union >= threshold;
2477
- }
2478
- function tokens(text) {
2479
- return new Set(
2480
- text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
2481
- );
2482
- }
2483
- function truncate2(text, max) {
2484
- return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
2485
- }
2486
-
2487
4187
  // src/reports/extract.ts
2488
4188
  var BULLET = /^(?:[-*•]|\d+[.)])\s+(.*)$/;
2489
4189
  function parseSections(raw) {
@@ -2533,6 +4233,7 @@ function extractReportFromText(raw, meta) {
2533
4233
  id: `${meta.taskId}-f-${index + 1}`,
2534
4234
  title: truncate3(text, 120),
2535
4235
  detail: text,
4236
+ description: text,
2536
4237
  severity: guessSeverity(text)
2537
4238
  }));
2538
4239
  return {
@@ -2543,7 +4244,8 @@ function extractReportFromText(raw, meta) {
2543
4244
  summary,
2544
4245
  findings,
2545
4246
  recommendations: recommendationTexts.map((t) => truncate3(t, 200)),
2546
- metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
4247
+ ...evidenceTexts.length > 0 ? { evidence: evidenceTexts.map((excerpt) => ({ kind: "observation", excerpt })) } : {},
4248
+ metadata: evidenceTexts.length > 0 ? { evidenceCount: evidenceTexts.length } : void 0,
2547
4249
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2548
4250
  };
2549
4251
  }
@@ -2552,6 +4254,7 @@ function truncate3(text, max) {
2552
4254
  }
2553
4255
 
2554
4256
  // src/reports/service.ts
4257
+ var MAX_REPORT_BYTES = 256 * 1024;
2555
4258
  var ReportService = class {
2556
4259
  constructor(store, log = logger) {
2557
4260
  this.store = store;
@@ -2559,27 +4262,56 @@ var ReportService = class {
2559
4262
  }
2560
4263
  store;
2561
4264
  log;
4265
+ /** Rejected reports from the last collection (§13 — malformed never enters aggregation). */
4266
+ lastDiagnostics = { collected: 0, accepted: 0, rejected: [] };
2562
4267
  get storeRef() {
2563
4268
  return this.store;
2564
4269
  }
2565
- /** Normalizes finished tasks into reports (§17 — normalizer). */
4270
+ /** Normalizes finished tasks into reports (§17 — normalizer), validating each
4271
+ * one so malformed reports never reach aggregation (§13). */
2566
4272
  async collectFromTasks(tasks) {
2567
- const reports = [];
4273
+ const candidates = [];
2568
4274
  for (const task of tasks) {
2569
4275
  const structured = task.result?.reports ?? [];
2570
4276
  if (structured.length > 0) {
2571
- reports.push(...structured);
4277
+ candidates.push(...structured);
2572
4278
  continue;
2573
4279
  }
2574
4280
  const text = task.result?.rawOutput ?? task.result?.summary ?? "";
2575
4281
  if (text.trim().length === 0) continue;
2576
- reports.push(
4282
+ candidates.push(
2577
4283
  extractReportFromText(text, {
2578
4284
  taskId: task.id,
2579
4285
  agent: task.agent ?? "unknown"
2580
4286
  })
2581
4287
  );
2582
4288
  }
4289
+ const reports = [];
4290
+ const rejected = [];
4291
+ for (const candidate of candidates) {
4292
+ const size = Buffer.byteLength(JSON.stringify(candidate), "utf8");
4293
+ if (size > MAX_REPORT_BYTES) {
4294
+ rejected.push({
4295
+ key: candidate?.id ?? "(unknown)",
4296
+ reason: `report exceeds the ${MAX_REPORT_BYTES} byte limit (${size} bytes)`,
4297
+ issues: []
4298
+ });
4299
+ this.log.warn(`Rejected oversized report (${size} bytes).`);
4300
+ continue;
4301
+ }
4302
+ const result = validateReport(candidate);
4303
+ if (result.valid) {
4304
+ reports.push(result.report);
4305
+ } else {
4306
+ rejected.push({
4307
+ key: candidate?.id ?? "(unknown)",
4308
+ reason: result.reason,
4309
+ issues: result.issues.map((issue) => `${issue.path}: ${issue.message}`)
4310
+ });
4311
+ this.log.warn(`Rejected malformed report: ${result.reason}`);
4312
+ }
4313
+ }
4314
+ this.lastDiagnostics = { collected: candidates.length, accepted: reports.length, rejected };
2583
4315
  return reports;
2584
4316
  }
2585
4317
  async saveReports(reports) {
@@ -2640,14 +4372,19 @@ var FileReportStore = class extends JsonFileStore {
2640
4372
 
2641
4373
  // src/pipelines/engine-factory.ts
2642
4374
  function buildPipelineEngine(config, log = logger) {
4375
+ const auditLog = new FileAuditLog();
2643
4376
  const agents = buildAgentRegistry(config, log);
2644
4377
  const roles = buildRoleRegistry(config);
2645
- const chat = createDefaultChatFn(config.project.providers);
4378
+ const chat = createDefaultChatFn(config.project.providers, { audit: auditLog });
2646
4379
  const tasks = new TaskEngine({
2647
4380
  agents,
2648
4381
  roles,
2649
4382
  store: new FileTaskStore(),
2650
- ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
4383
+ ...config.project.memory !== void 0 ? {
4384
+ memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
4385
+ required: config.project.memory.required
4386
+ })
4387
+ } : {},
2651
4388
  audit: new FileAuditLog(),
2652
4389
  log
2653
4390
  });
@@ -2683,14 +4420,68 @@ var ProjectStores = class {
2683
4420
  this.audit = new FileAuditLog(directory);
2684
4421
  }
2685
4422
  };
4423
+ var STORE_FILES = [
4424
+ "tasks.json",
4425
+ "pipelines.json",
4426
+ "reports.json",
4427
+ "plans.json",
4428
+ "memory.json"
4429
+ ];
4430
+ async function migrateProjectStores(directory = `${process.cwd()}/.omninode`, options = {}) {
4431
+ const reports = [];
4432
+ for (const file of STORE_FILES) {
4433
+ const path2 = `${directory}/${file}`;
4434
+ const state = await inspectDocument(path2);
4435
+ if (state.status === "missing") {
4436
+ reports.push({ file, state: state.status, migrated: false });
4437
+ continue;
4438
+ }
4439
+ if (state.status === "current") {
4440
+ reports.push({ file, state: state.status, before: state.count, after: state.count, migrated: false });
4441
+ continue;
4442
+ }
4443
+ if (state.status === "future") {
4444
+ reports.push({
4445
+ file,
4446
+ state: state.status,
4447
+ migrated: false,
4448
+ note: `written by a newer OmniNode (schema v${state.schemaVersion}); not touched`
4449
+ });
4450
+ continue;
4451
+ }
4452
+ if (state.status === "corrupt") {
4453
+ reports.push({ file, state: state.status, migrated: false, note: state.detail });
4454
+ continue;
4455
+ }
4456
+ const items = await readDocument(path2, void 0, false);
4457
+ if (!options.checkOnly) {
4458
+ await writeFileAtomic(path2, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, items }, null, 2)}
4459
+ `);
4460
+ }
4461
+ const beforeCount = items.length;
4462
+ reports.push({
4463
+ file,
4464
+ state: state.status,
4465
+ before: beforeCount,
4466
+ after: items.length,
4467
+ migrated: !options.checkOnly,
4468
+ ...options.checkOnly ? { note: "would migrate" } : {}
4469
+ });
4470
+ }
4471
+ return reports;
4472
+ }
2686
4473
 
2687
4474
  // src/version.ts
2688
- var OMNINODE_VERSION = "2.0.0-alpha.2";
4475
+ var OMNINODE_VERSION = "2.0.0-alpha.20";
2689
4476
  export {
4477
+ AgentAdapterRegistry,
2690
4478
  AgentError,
2691
4479
  AgentRegistry,
2692
4480
  ConfigError,
2693
4481
  ConsoleLogSink,
4482
+ DEFAULT_AUDIT_MAX_BYTES,
4483
+ DEFAULT_MAX_OUTPUT_BYTES,
4484
+ DEFAULT_MAX_PARALLEL_STEPS,
2694
4485
  FileAuditLog,
2695
4486
  FilePipelineRunStore,
2696
4487
  FilePlanStore,
@@ -2700,6 +4491,11 @@ export {
2700
4491
  JsonFileStore,
2701
4492
  LocalMemoryProvider,
2702
4493
  Logger,
4494
+ MAX_COMBINED_CONTEXT_CHARS,
4495
+ MAX_COMBINED_FINDINGS,
4496
+ MAX_CONTEXT_CHARS,
4497
+ MAX_MESSAGE_BYTES,
4498
+ MAX_REPORT_BYTES,
2703
4499
  MemoryError,
2704
4500
  MemoryService,
2705
4501
  ModelPlanner,
@@ -2709,53 +4505,96 @@ export {
2709
4505
  OmniHilbrasProvider,
2710
4506
  OmniNodeError,
2711
4507
  OpenAICompatibleProvider,
4508
+ PROTOCOL_V2,
2712
4509
  PipelineEngine,
2713
4510
  PipelineError,
2714
4511
  PlannerError,
2715
4512
  ProcessAgent,
4513
+ ProcessAgentAdapter,
2716
4514
  ProjectStores,
4515
+ ProtocolDecoder,
2717
4516
  ProtocolError,
4517
+ ProtocolSession,
2718
4518
  ProviderError,
2719
4519
  RememberaMemoryProvider,
2720
4520
  ReportService,
2721
4521
  RoleRegistry,
2722
- TERMINAL_TASK_STATUSES,
4522
+ SCHEMA_VERSION,
4523
+ SUPPORTED_PROTOCOL_VERSIONS,
2723
4524
  TaskEngine,
2724
4525
  TaskError,
2725
4526
  agentConfigSchema,
2726
4527
  aggregateReports,
2727
4528
  appConfigSchema,
2728
4529
  authHeaders,
4530
+ budgetContext,
2729
4531
  buildAgentRegistry,
2730
4532
  buildChildEnv,
4533
+ buildHello,
4534
+ buildHelloAck,
2731
4535
  buildPipelineEngine,
2732
4536
  buildPlanner,
2733
4537
  buildPlannerContext,
2734
4538
  buildRoleRegistry,
2735
4539
  composeTaskText,
4540
+ configDiagnostics,
4541
+ configureLogger,
2736
4542
  createAgent,
2737
4543
  createDefaultChatFn,
2738
4544
  createMemoryProvider,
2739
4545
  createProvider,
2740
4546
  createTaskEngine,
4547
+ decodeMessage,
4548
+ defaultAgentAdapters,
2741
4549
  defaultProjectConfigYaml,
4550
+ describeExit,
4551
+ describeSecretFindings,
4552
+ detectConflicts,
4553
+ encodeMessage,
4554
+ envelope,
2742
4555
  expandEnvRefs,
4556
+ extractMessage,
2743
4557
  extractReportFromText,
2744
4558
  findConfigFile,
4559
+ inspectDocument,
4560
+ isNetworkError,
2745
4561
  isOmniNodeError,
2746
- keywords,
4562
+ isRetryable,
4563
+ isSupportedProtocol,
4564
+ isTimeoutError,
4565
+ killTree,
4566
+ kindFromStatus,
2747
4567
  loadConfig,
4568
+ loadConfigDetailed,
2748
4569
  logger,
4570
+ migrateProjectStores,
4571
+ newMessageId,
4572
+ normalizeEvidence,
4573
+ normalizeFinding,
4574
+ parseHello,
2749
4575
  parsePlanJson,
4576
+ parseRetryAfter,
2750
4577
  pipelineConfigSchema,
2751
4578
  pipelineStepConfigSchema,
4579
+ planJsonSchema,
2752
4580
  plannerConfigSchema,
2753
4581
  projectConfigSchema,
2754
4582
  providerConfigSchema,
4583
+ providerHttpError,
4584
+ providerTransportError,
4585
+ readDocument,
2755
4586
  registryKey,
2756
4587
  requestJson,
2757
4588
  resolveApiKey,
4589
+ resolveEnvPolicy,
2758
4590
  roleConfigSchema,
2759
- similar
4591
+ sanitizeChunk,
4592
+ scanForInlineSecrets,
4593
+ similar,
4594
+ validateEnvelope,
4595
+ validatePlan,
4596
+ validateReport,
4597
+ validateWorkingDirectory,
4598
+ writeFileAtomic
2760
4599
  };
2761
4600
  //# sourceMappingURL=index.js.map