@hilbras/omninode 2.0.0-alpha.1 → 2.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/version.ts
9
- var OMNINODE_VERSION = "2.0.0-alpha.1";
9
+ var OMNINODE_VERSION = "2.0.0-alpha.10";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -44,11 +44,6 @@ var PipelineError = class extends OmniNodeError {
44
44
  super(code, message, options);
45
45
  }
46
46
  };
47
- var ProtocolError = class extends OmniNodeError {
48
- constructor(message, options = {}) {
49
- super("PROTOCOL_VIOLATION", message, options);
50
- }
51
- };
52
47
  var PlannerError = class extends OmniNodeError {
53
48
  constructor(code, message, options = {}) {
54
49
  super(code, message, options);
@@ -73,6 +68,7 @@ var providerConfigSchema = z.object({
73
68
  type: z.enum(["openai-compatible", "omnihilbras", "openrouter", "local", "custom"]),
74
69
  base_url: z.string().url(),
75
70
  api_key_env_var: z.string().min(1).optional(),
71
+ timeout_ms: z.number().int().positive().optional(),
76
72
  headers: z.record(z.string()).optional(),
77
73
  enabled: z.boolean().optional(),
78
74
  metadata: z.record(z.unknown()).optional()
@@ -88,6 +84,11 @@ var agentConfigSchema = z.object({
88
84
  input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
89
85
  timeout_ms: z.number().int().positive().optional(),
90
86
  inherit_env: z.boolean().optional(),
87
+ env_policy: z.enum(["inherit", "allowlist", "denylist", "explicit"]).optional(),
88
+ env_allowlist: z.array(z.string()).optional(),
89
+ env_denylist: z.array(z.string()).optional(),
90
+ max_output_bytes: z.number().int().positive().optional(),
91
+ allow_external_cwd: z.boolean().optional(),
91
92
  metadata: z.record(z.unknown()).optional()
92
93
  }).strict();
93
94
  var roleConfigSchema = z.object({
@@ -132,7 +133,9 @@ var projectConfigSchema = z.object({
132
133
  memory: z.object({
133
134
  provider: z.string().min(1).default("local"),
134
135
  base_url: z.string().url().optional(),
135
- api_key_env_var: z.string().min(1).optional()
136
+ api_key_env_var: z.string().min(1).optional(),
137
+ /** When true, memory failures fail tasks instead of being tolerated (§12). */
138
+ required: z.boolean().optional()
136
139
  }).strict().optional()
137
140
  }).strict();
138
141
  var appConfigSchema = z.object({
@@ -214,7 +217,8 @@ function toAppConfig(yaml) {
214
217
  memory: {
215
218
  provider: yaml.project.memory.provider,
216
219
  baseUrl: yaml.project.memory.base_url,
217
- apiKeyEnvVar: yaml.project.memory.api_key_env_var
220
+ apiKeyEnvVar: yaml.project.memory.api_key_env_var,
221
+ required: yaml.project.memory.required
218
222
  }
219
223
  } : {}
220
224
  },
@@ -245,6 +249,7 @@ function toProviderConfig(raw) {
245
249
  type: raw.type,
246
250
  baseUrl: raw.base_url,
247
251
  apiKeyEnvVar: raw.api_key_env_var,
252
+ timeoutMs: raw.timeout_ms,
248
253
  headers: raw.headers,
249
254
  enabled: raw.enabled,
250
255
  metadata: raw.metadata
@@ -262,6 +267,11 @@ function toAgentConfig(raw) {
262
267
  inputMode: raw.input_mode,
263
268
  timeoutMs: raw.timeout_ms,
264
269
  inheritEnv: raw.inherit_env,
270
+ envPolicy: raw.env_policy,
271
+ envAllowlist: raw.env_allowlist,
272
+ envDenylist: raw.env_denylist,
273
+ maxOutputBytes: raw.max_output_bytes,
274
+ allowExternalCwd: raw.allow_external_cwd,
265
275
  metadata: raw.metadata
266
276
  };
267
277
  }
@@ -358,35 +368,32 @@ function authHeaders(config, env = process.env) {
358
368
  }
359
369
 
360
370
  // src/providers/http.ts
371
+ var HttpResponseError = class extends Error {
372
+ constructor(message, status, cause) {
373
+ super(message, { cause });
374
+ this.status = status;
375
+ this.name = "HttpResponseError";
376
+ }
377
+ status;
378
+ };
361
379
  var DEFAULT_TIMEOUT_MS = 3e4;
362
380
  async function requestJson(url, options = {}) {
363
381
  const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
364
- let response;
365
- try {
366
- response = await fetch(url, {
367
- method,
368
- headers: {
369
- accept: "application/json",
370
- ...body !== void 0 ? { "content-type": "application/json" } : {},
371
- ...headers
372
- },
373
- ...body !== void 0 ? { body: JSON.stringify(body) } : {},
374
- signal: AbortSignal.timeout(timeoutMs)
375
- });
376
- } catch (error) {
377
- const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
378
- throw new ProviderError("PROVIDER_UNAVAILABLE", `Request to ${url} failed: ${reason}.`, {
379
- cause: error
380
- });
381
- }
382
+ const response = await fetch(url, {
383
+ method,
384
+ headers: {
385
+ accept: "application/json",
386
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
387
+ ...headers
388
+ },
389
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
390
+ signal: AbortSignal.timeout(timeoutMs)
391
+ });
382
392
  let parsed;
383
393
  try {
384
394
  parsed = await response.json();
385
395
  } catch (error) {
386
- throw new ProviderError("PROVIDER_UNAVAILABLE", `Response from ${url} was not valid JSON.`, {
387
- cause: error,
388
- details: { status: response.status }
389
- });
396
+ throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
390
397
  }
391
398
  return { status: response.status, body: parsed };
392
399
  }
@@ -447,24 +454,167 @@ var Logger = class _Logger {
447
454
  };
448
455
  var logger = new Logger();
449
456
 
457
+ // src/providers/errors.ts
458
+ function kindToCode(kind) {
459
+ switch (kind) {
460
+ case "AUTHENTICATION_ERROR":
461
+ return "PROVIDER_AUTH_FAILED";
462
+ case "MODEL_NOT_FOUND":
463
+ return "MODEL_NOT_FOUND";
464
+ default:
465
+ return "PROVIDER_UNAVAILABLE";
466
+ }
467
+ }
468
+ function isRetryable(kind) {
469
+ return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
470
+ }
471
+ function kindFromStatus(status) {
472
+ if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
473
+ if (status === 429) return "RATE_LIMIT_ERROR";
474
+ if (status === 404) return "MODEL_NOT_FOUND";
475
+ if (status === 408 || status === 504) return "TIMEOUT";
476
+ if (status >= 500) return "SERVER_ERROR";
477
+ if (status >= 400) return "INVALID_REQUEST";
478
+ return "UNKNOWN_ERROR";
479
+ }
480
+ function extractMessage(body) {
481
+ if (body === null || typeof body !== "object") return void 0;
482
+ const root = body;
483
+ const error = root.error ?? root;
484
+ const message = error.message ?? error.detail ?? root.message;
485
+ return typeof message === "string" ? message : void 0;
486
+ }
487
+ function parseRetryAfter(value, now = Date.now()) {
488
+ if (!value) return void 0;
489
+ const seconds = Number(value);
490
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
491
+ const date = Date.parse(value);
492
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
493
+ }
494
+ function isTimeoutError(error) {
495
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
496
+ }
497
+ function fallbackMessage(kind, context, status) {
498
+ const { provider, operation } = context;
499
+ switch (kind) {
500
+ case "AUTHENTICATION_ERROR":
501
+ return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
502
+ case "RATE_LIMIT_ERROR":
503
+ return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
504
+ case "MODEL_NOT_FOUND":
505
+ return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
506
+ case "TIMEOUT":
507
+ return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
508
+ case "SERVER_ERROR":
509
+ return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
510
+ case "INVALID_REQUEST":
511
+ return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
512
+ default:
513
+ return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
514
+ }
515
+ }
516
+ function providerHttpError(context) {
517
+ const status = context.status ?? 0;
518
+ const kind = kindFromStatus(status);
519
+ const detail = extractMessage(context.body);
520
+ const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
521
+ return new ProviderError(
522
+ context.code ?? kindToCode(kind),
523
+ detail ?? fallbackMessage(kind, context, status),
524
+ {
525
+ cause: context.cause,
526
+ details: {
527
+ kind,
528
+ provider: context.provider,
529
+ operation: context.operation,
530
+ status,
531
+ retryable: isRetryable(kind),
532
+ ...retryAfterMs !== void 0 ? { retryAfterMs } : {},
533
+ ...detail ? { providerMessage: detail } : {}
534
+ }
535
+ }
536
+ );
537
+ }
538
+ function providerTransportError(context) {
539
+ const cause = context.cause;
540
+ const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
541
+ const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
542
+ return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
543
+ cause: context.cause,
544
+ details: {
545
+ kind,
546
+ provider: context.provider,
547
+ operation: context.operation,
548
+ retryable: isRetryable(kind)
549
+ }
550
+ });
551
+ }
552
+
450
553
  // src/providers/openai-compatible/index.ts
554
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
451
555
  var OpenAICompatibleProvider = class {
452
556
  config;
557
+ providerId;
558
+ authentication;
453
559
  log;
454
- constructor(config, log = logger) {
560
+ /** Discovered models, so getModel() answers without another round-trip. */
561
+ discovered;
562
+ env;
563
+ constructor(config, log = logger, env = process.env) {
455
564
  this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
565
+ this.env = env;
566
+ this.providerId = config.providerId ?? config.name;
567
+ this.authentication = {
568
+ method: config.apiKeyEnvVar ? "bearer" : "none",
569
+ ...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
570
+ configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
571
+ };
456
572
  this.log = log.child({ provider: config.name });
457
573
  }
458
- async listModels() {
459
- const { status, body } = await requestJson(`${this.config.baseUrl}/models`, {
460
- headers: authHeaders(this.config)
461
- });
462
- if (status === 401 || status === 403) {
463
- throw authFailed(this.config, status);
574
+ /** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
575
+ get requestTimeoutMs() {
576
+ return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
577
+ }
578
+ /** Authenticate + discover (roadmap §10, Model Discovery). */
579
+ async connect() {
580
+ authHeaders(this.config, this.env);
581
+ const models = await this.listModels();
582
+ this.discovered = models;
583
+ return models;
584
+ }
585
+ async getModel(modelId) {
586
+ const models = this.discovered ?? await this.connect();
587
+ return models.find((m) => m.id === modelId);
588
+ }
589
+ /** Raw /models body (protected so adapters can capture gateway-level metadata). */
590
+ async rawModelsResponse() {
591
+ const response = await this.modelsRequest();
592
+ return response.body;
593
+ }
594
+ async modelsRequest() {
595
+ let response;
596
+ try {
597
+ response = await requestJson(`${this.config.baseUrl}/models`, {
598
+ headers: authHeaders(this.config, this.env),
599
+ timeoutMs: this.requestTimeoutMs
600
+ });
601
+ } catch (error) {
602
+ if (error instanceof ProviderError) throw error;
603
+ throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
464
604
  }
605
+ const { status, body } = response;
465
606
  if (status !== 200) {
466
- throw unavailable(this.config, `Model discovery returned HTTP ${status}.`, status);
607
+ throw providerHttpError({
608
+ provider: this.config.name,
609
+ operation: "listModels",
610
+ status,
611
+ body
612
+ });
467
613
  }
614
+ return response;
615
+ }
616
+ async listModels() {
617
+ const body = await this.rawModelsResponse();
468
618
  const entries = extractModelEntries(body, this.config);
469
619
  const models = [];
470
620
  let skipped = 0;
@@ -473,22 +623,44 @@ var OpenAICompatibleProvider = class {
473
623
  skipped += 1;
474
624
  continue;
475
625
  }
476
- models.push({
477
- provider: this.config.name,
478
- id: entry2.id,
479
- displayName: entry2.id,
480
- status: "available",
481
- metadata: {
482
- ...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
483
- ...typeof entry2.created === "number" ? { createdAt: entry2.created } : {}
484
- }
485
- });
626
+ models.push(this.toModelInfo(entry2));
486
627
  }
487
628
  if (skipped > 0) {
488
629
  this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
489
630
  }
490
631
  return models;
491
632
  }
633
+ /** Maps a raw model entry onto the normalized metadata shape (§10). */
634
+ toModelInfo(entry2) {
635
+ const parameters = Array.isArray(entry2.supported_parameters) ? entry2.supported_parameters.filter((p) => typeof p === "string") : void 0;
636
+ const contextWindow = typeof entry2.context_length === "number" ? entry2.context_length : typeof entry2.context_window === "number" ? entry2.context_window : void 0;
637
+ const inputTypes = normalizeModalities(entry2.input_modalities);
638
+ const outputTypes = normalizeModalities(entry2.output_modalities);
639
+ return {
640
+ provider: this.config.name,
641
+ id: entry2.id,
642
+ displayName: entry2.id,
643
+ ...typeof entry2.name === "string" ? { name: entry2.name } : {},
644
+ status: "available",
645
+ ...contextWindow !== void 0 ? { contextWindow } : {},
646
+ ...inputTypes !== void 0 ? { inputTypes } : {},
647
+ ...outputTypes !== void 0 ? { outputTypes } : {},
648
+ ...parameters !== void 0 ? {
649
+ supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
650
+ supportsStructuredOutput: parameters.includes("response_format"),
651
+ supportsStreaming: parameters.includes("stream")
652
+ } : {},
653
+ metadata: {
654
+ ...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
655
+ ...typeof entry2.created === "number" ? { createdAt: entry2.created } : {},
656
+ ...parameters !== void 0 ? { supportedParameters: parameters } : {}
657
+ }
658
+ };
659
+ }
660
+ /** Streaming where the gateway supports it (§11). */
661
+ stream(request, onChunk) {
662
+ return streamChatCompletion(this.config, request, onChunk, this.env);
663
+ }
492
664
  async healthCheck() {
493
665
  const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
494
666
  try {
@@ -518,27 +690,35 @@ var OpenAICompatibleProvider = class {
518
690
  }
519
691
  }
520
692
  async chat(request) {
521
- const { status, body } = await requestJson(`${this.config.baseUrl}/chat/completions`, {
522
- method: "POST",
523
- headers: authHeaders(this.config),
524
- body: {
525
- model: request.model,
526
- messages: request.messages,
527
- ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
528
- ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
529
- ...request.stop !== void 0 ? { stop: request.stop } : {}
530
- }
531
- });
532
- if (status === 401 || status === 403) throw authFailed(this.config, status);
533
- if (status === 404) {
534
- throw new ProviderError(
535
- "MODEL_NOT_FOUND",
536
- `Model "${request.model}" or the chat endpoint was not found on provider "${this.config.name}" (HTTP 404).`,
537
- { details: { status } }
538
- );
693
+ let response;
694
+ try {
695
+ response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
696
+ method: "POST",
697
+ headers: authHeaders(this.config, this.env),
698
+ timeoutMs: this.requestTimeoutMs,
699
+ body: {
700
+ model: request.model,
701
+ messages: request.messages,
702
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
703
+ ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
704
+ ...request.stop !== void 0 ? { stop: request.stop } : {}
705
+ }
706
+ });
707
+ } catch (error) {
708
+ if (error instanceof ProviderError) throw error;
709
+ throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
539
710
  }
711
+ const { status, body } = response;
540
712
  if (status !== 200) {
541
- throw unavailable(this.config, `Chat completion returned HTTP ${status}.`, status);
713
+ throw providerHttpError({
714
+ provider: this.config.name,
715
+ operation: "chat",
716
+ status,
717
+ body,
718
+ ...status === 404 ? {
719
+ code: "MODEL_NOT_FOUND"
720
+ } : {}
721
+ });
542
722
  }
543
723
  const payload = body;
544
724
  const choices = Array.isArray(payload.choices) ? payload.choices : [];
@@ -560,6 +740,81 @@ var OpenAICompatibleProvider = class {
560
740
  };
561
741
  }
562
742
  };
743
+ async function streamChatCompletion(config, request, onChunk, env = process.env) {
744
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
745
+ const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
746
+ let response;
747
+ try {
748
+ response = await fetch(`${baseUrl}/chat/completions`, {
749
+ method: "POST",
750
+ headers: {
751
+ accept: "text/event-stream",
752
+ "content-type": "application/json",
753
+ ...authHeaders(config, env)
754
+ },
755
+ body: JSON.stringify({
756
+ model: request.model,
757
+ messages: request.messages,
758
+ stream: true,
759
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
760
+ ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
761
+ }),
762
+ signal: AbortSignal.timeout(timeoutMs)
763
+ });
764
+ } catch (error) {
765
+ throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
766
+ }
767
+ if (!response.ok) {
768
+ const body = await response.json().catch(() => void 0);
769
+ throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
770
+ }
771
+ if (!response.body) {
772
+ throw providerHttpError({
773
+ provider: config.name,
774
+ operation: "stream",
775
+ status: 500,
776
+ body: { error: { message: "streaming is not supported by this endpoint" } }
777
+ });
778
+ }
779
+ let content = "";
780
+ let model = request.model;
781
+ let finishReason;
782
+ const decoder = new TextDecoder();
783
+ let buffer = "";
784
+ for await (const piece of response.body) {
785
+ buffer += decoder.decode(piece, { stream: true });
786
+ let newline = buffer.indexOf("\n");
787
+ while (newline >= 0) {
788
+ const line = buffer.slice(0, newline).trim();
789
+ buffer = buffer.slice(newline + 1);
790
+ newline = buffer.indexOf("\n");
791
+ if (line.length === 0 || line.startsWith(":")) continue;
792
+ const data = line.startsWith("data:") ? line.slice(5).trim() : line;
793
+ if (data === "[DONE]") continue;
794
+ let event;
795
+ try {
796
+ event = JSON.parse(data);
797
+ } catch {
798
+ continue;
799
+ }
800
+ if (typeof event.model === "string") model = event.model;
801
+ const choices = Array.isArray(event.choices) ? event.choices : [];
802
+ const first = choices[0];
803
+ const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
804
+ if (delta.length > 0) content += delta;
805
+ if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
806
+ onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
807
+ }
808
+ }
809
+ return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
810
+ }
811
+ function normalizeModalities(value) {
812
+ const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
813
+ if (!list) return void 0;
814
+ const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
815
+ const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
816
+ return modalities.length > 0 ? modalities : void 0;
817
+ }
563
818
  function normalizeBaseUrl(baseUrl) {
564
819
  return baseUrl.replace(/\/+$/, "");
565
820
  }
@@ -574,14 +829,6 @@ function extractModelEntries(body, config) {
574
829
  { details: { endpoint: "models" } }
575
830
  );
576
831
  }
577
- function authFailed(config, status) {
578
- const envHint = config.apiKeyEnvVar ? ` Check that $${config.apiKeyEnvVar} is set and valid.` : " The provider requires authentication; configure api_key_env_var for it.";
579
- return new ProviderError(
580
- "PROVIDER_AUTH_FAILED",
581
- `Authentication with provider "${config.name}" failed (HTTP ${status}).${envHint}`,
582
- { details: { status } }
583
- );
584
- }
585
832
  function unavailable(config, message, status) {
586
833
  return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
587
834
  details: { status }
@@ -594,11 +841,33 @@ function inferHealth(error) {
594
841
 
595
842
  // src/providers/omnihilbras/index.ts
596
843
  var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
844
+ gateway;
845
+ async listModels() {
846
+ const raw = await this.rawModelsResponse();
847
+ await this.extractGatewayMetadata(raw);
848
+ const models = await super.listModels();
849
+ return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
850
+ }
597
851
  /**
598
852
  * Run the full connect flow. Throws on authentication or transport failure
599
853
  * (use healthCheck() for a non-throwing probe).
600
854
  */
601
- async connect(registry) {
855
+ /**
856
+ * Gateway metadata reported alongside the model list (gateway version, tier,
857
+ * region…), kept separate from per-model metadata (§11 — provider metadata).
858
+ */
859
+ gatewayMetadata() {
860
+ return this.gateway;
861
+ }
862
+ async extractGatewayMetadata(body) {
863
+ if (body !== null && typeof body === "object") {
864
+ const gateway = body.gateway;
865
+ if (gateway !== null && typeof gateway === "object") {
866
+ this.gateway = gateway;
867
+ }
868
+ }
869
+ }
870
+ async connectAndRegister(registry) {
602
871
  const apiKey = resolveApiKey(this.config);
603
872
  const discovered = await this.listModels();
604
873
  const seen = /* @__PURE__ */ new Set();
@@ -822,7 +1091,7 @@ async function testProvider(name, options = {}) {
822
1091
  if (options.connect && provider instanceof OmniHilbrasProvider) {
823
1092
  const registry = new ModelRegistry();
824
1093
  try {
825
- const result = await provider.connect(registry);
1094
+ const result = await provider.connectAndRegister(registry);
826
1095
  printProviderStatus(result.status);
827
1096
  console.log(` registered: ${result.registered}`);
828
1097
  } catch (error) {
@@ -897,19 +1166,416 @@ function registerModelsCommand(program) {
897
1166
 
898
1167
  // src/agents/process/index.ts
899
1168
  import { spawn } from "child_process";
900
- var DEFAULT_TIMEOUT_MS2 = 6e5;
901
- var KILL_GRACE_MS = 5e3;
1169
+
1170
+ // src/agent-protocol/version.ts
1171
+ var PROTOCOL_V2 = "omninode-agent-protocol/2";
1172
+ var SUPPORTED_PROTOCOL_VERSIONS = [
1173
+ "omninode-agent-protocol/1",
1174
+ PROTOCOL_V2
1175
+ ];
1176
+ var MAX_MESSAGE_BYTES = 1048576;
1177
+ function isSupportedProtocol(protocol) {
1178
+ return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
1179
+ }
1180
+
1181
+ // src/agent-protocol/types.ts
1182
+ function newMessageId(prefix = "msg") {
1183
+ const random = Math.random().toString(36).slice(2, 10);
1184
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
1185
+ }
1186
+ function envelope(type, payload, ids = {}) {
1187
+ return {
1188
+ protocol: PROTOCOL_V2,
1189
+ messageId: newMessageId(),
1190
+ ...ids.requestId !== void 0 ? { requestId: ids.requestId } : {},
1191
+ ...ids.taskId !== void 0 ? { taskId: ids.taskId } : {},
1192
+ ...ids.agentId !== void 0 ? { agentId: ids.agentId } : {},
1193
+ ...ids.pipelineId !== void 0 ? { pipelineId: ids.pipelineId } : {},
1194
+ ...ids.executionId !== void 0 ? { executionId: ids.executionId } : {},
1195
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1196
+ type,
1197
+ payload,
1198
+ ...ids.metadata !== void 0 ? { metadata: ids.metadata } : {}
1199
+ };
1200
+ }
1201
+
1202
+ // src/agent-protocol/codec.ts
1203
+ var KNOWN_TYPES = /* @__PURE__ */ new Set([
1204
+ // node → agent
1205
+ "TASK",
1206
+ "HELLO_ACK",
1207
+ "CONTEXT",
1208
+ "INSTRUCTION",
1209
+ "RESPONSE",
1210
+ "CANCEL",
1211
+ // agent → node
1212
+ "HELLO",
1213
+ "TASK_REQUEST",
1214
+ "TASK_ACCEPTED",
1215
+ "TASK_STARTED",
1216
+ "STATUS",
1217
+ "QUESTION",
1218
+ "REPORT",
1219
+ "ARTIFACT",
1220
+ "COMPLETION",
1221
+ "ERROR",
1222
+ "CANCELLED"
1223
+ ]);
1224
+ var PAYLOAD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["TASK", "REPORT", "COMPLETION", "ERROR", "QUESTION", "ARTIFACT", "HELLO"]);
1225
+ function validateEnvelope(value, line) {
1226
+ const sample = line?.slice(0, 200);
1227
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1228
+ return { ok: false, error: { code: "NOT_AN_OBJECT", message: "message must be a JSON object", ...sample ? { sample } : {} } };
1229
+ }
1230
+ const raw = value;
1231
+ const protocol = raw.protocol;
1232
+ if (protocol === void 0) {
1233
+ const type = raw.type;
1234
+ if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
1235
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(type)}`, ...sample ? { sample } : {} } };
1236
+ }
1237
+ return {
1238
+ ok: true,
1239
+ legacy: true,
1240
+ envelope: {
1241
+ protocol: "omninode-agent-protocol/1",
1242
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "legacy",
1243
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1244
+ type,
1245
+ payload: raw.payload
1246
+ }
1247
+ };
1248
+ }
1249
+ if (typeof protocol !== "string" || !isSupportedProtocol(protocol)) {
1250
+ return {
1251
+ ok: false,
1252
+ error: { code: "UNSUPPORTED_VERSION", message: `unsupported protocol "${String(protocol)}"`, ...sample ? { sample } : {} }
1253
+ };
1254
+ }
1255
+ if (raw.type === void 0) {
1256
+ return { ok: false, error: { code: "MISSING_FIELD", message: "message is missing 'type'", ...sample ? { sample } : {} } };
1257
+ }
1258
+ if (typeof raw.type !== "string" || !KNOWN_TYPES.has(raw.type)) {
1259
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(raw.type)}`, ...sample ? { sample } : {} } };
1260
+ }
1261
+ if (raw.timestamp !== void 0 && typeof raw.timestamp !== "string") {
1262
+ return { ok: false, error: { code: "MISSING_FIELD", message: "'timestamp' must be a string", ...sample ? { sample } : {} } };
1263
+ }
1264
+ if (PAYLOAD_REQUIRED_TYPES.has(raw.type) && (raw.payload === void 0 || raw.payload === null)) {
1265
+ return { ok: false, error: { code: "INVALID_PAYLOAD", message: `${raw.type} requires a payload`, ...sample ? { sample } : {} } };
1266
+ }
1267
+ return {
1268
+ ok: true,
1269
+ legacy: protocol !== PROTOCOL_V2,
1270
+ envelope: {
1271
+ protocol,
1272
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "unknown",
1273
+ ...typeof raw.requestId === "string" ? { requestId: raw.requestId } : {},
1274
+ ...typeof raw.taskId === "string" ? { taskId: raw.taskId } : {},
1275
+ ...typeof raw.agentId === "string" ? { agentId: raw.agentId } : {},
1276
+ ...typeof raw.pipelineId === "string" ? { pipelineId: raw.pipelineId } : {},
1277
+ ...typeof raw.executionId === "string" ? { executionId: raw.executionId } : {},
1278
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
1279
+ type: raw.type,
1280
+ payload: raw.payload,
1281
+ ...raw.metadata !== null && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}
1282
+ }
1283
+ };
1284
+ }
1285
+ function decodeMessage(line, maxBytes = MAX_MESSAGE_BYTES) {
1286
+ const trimmed = line.trim();
1287
+ if (trimmed.length === 0) {
1288
+ return { ok: false, error: { code: "INVALID_JSON", message: "empty line" } };
1289
+ }
1290
+ if (trimmed.length > maxBytes) {
1291
+ return {
1292
+ ok: false,
1293
+ error: { code: "OVERSIZED", message: `message of ${trimmed.length} bytes exceeds the ${maxBytes} byte limit`, sample: trimmed.slice(0, 200) }
1294
+ };
1295
+ }
1296
+ let value;
1297
+ try {
1298
+ value = JSON.parse(trimmed);
1299
+ } catch (error) {
1300
+ return {
1301
+ ok: false,
1302
+ error: { code: "INVALID_JSON", message: error instanceof Error ? error.message : "invalid JSON", sample: trimmed.slice(0, 200) }
1303
+ };
1304
+ }
1305
+ return validateEnvelope(value, trimmed);
1306
+ }
1307
+ function encodeMessage(envelope2, maxBytes = MAX_MESSAGE_BYTES) {
1308
+ const line = JSON.stringify(envelope2);
1309
+ if (line.length > maxBytes) {
1310
+ throw new Error(`outbound protocol message of ${line.length} bytes exceeds the ${maxBytes} byte limit`);
1311
+ }
1312
+ return line;
1313
+ }
1314
+ var ProtocolDecoder = class {
1315
+ buffer = "";
1316
+ maxBytes;
1317
+ constructor(maxBytes = MAX_MESSAGE_BYTES) {
1318
+ this.maxBytes = maxBytes;
1319
+ }
1320
+ push(chunk) {
1321
+ this.buffer += chunk;
1322
+ const results = [];
1323
+ let index = this.buffer.indexOf("\n");
1324
+ while (index >= 0) {
1325
+ const line = this.buffer.slice(0, index);
1326
+ this.buffer = this.buffer.slice(index + 1);
1327
+ if (line.trim().length > 0) results.push(decodeMessage(line, this.maxBytes));
1328
+ index = this.buffer.indexOf("\n");
1329
+ }
1330
+ if (this.buffer.length > this.maxBytes) {
1331
+ results.push({
1332
+ ok: false,
1333
+ error: { code: "OVERSIZED", message: `line exceeded ${this.maxBytes} bytes without a newline`, sample: this.buffer.slice(0, 200) }
1334
+ });
1335
+ this.buffer = "";
1336
+ }
1337
+ return results;
1338
+ }
1339
+ /** Flush any trailing line when the process exits. */
1340
+ flush() {
1341
+ if (this.buffer.trim().length === 0) {
1342
+ this.buffer = "";
1343
+ return [];
1344
+ }
1345
+ const result = decodeMessage(this.buffer, this.maxBytes);
1346
+ this.buffer = "";
1347
+ return [result];
1348
+ }
1349
+ };
1350
+
1351
+ // src/agent-protocol/handshake.ts
1352
+ function parseHello(envelopeValue) {
1353
+ if (envelopeValue.type !== "HELLO") {
1354
+ return { ok: false, reason: `expected HELLO, received ${envelopeValue.type}` };
1355
+ }
1356
+ const payload = envelopeValue.payload;
1357
+ if (payload === null || typeof payload !== "object") {
1358
+ return { ok: false, reason: "HELLO payload must be an object" };
1359
+ }
1360
+ const p = payload;
1361
+ if (typeof p.name !== "string" || p.name.length === 0) {
1362
+ return { ok: false, reason: "HELLO payload is missing 'name'" };
1363
+ }
1364
+ return {
1365
+ ok: true,
1366
+ descriptor: {
1367
+ protocol: envelopeValue.protocol,
1368
+ name: p.name,
1369
+ ...typeof p.version === "string" ? { version: p.version } : {},
1370
+ capabilities: Array.isArray(p.capabilities) ? p.capabilities.filter((c) => typeof c === "string") : [],
1371
+ supports: Array.isArray(p.supports) ? p.supports.filter((s) => typeof s === "string") : []
1372
+ }
1373
+ };
1374
+ }
1375
+ function buildHelloAck(descriptor) {
1376
+ return envelope("HELLO_ACK", {
1377
+ ok: true,
1378
+ protocol: PROTOCOL_V2,
1379
+ agent: descriptor.name,
1380
+ ...descriptor.version !== void 0 ? { agentVersion: descriptor.version } : {},
1381
+ supportedProtocols: [PROTOCOL_V2]
1382
+ }, { agentId: descriptor.name });
1383
+ }
1384
+
1385
+ // src/agent-protocol/session.ts
1386
+ var ProtocolSession = class {
1387
+ decoder;
1388
+ ids;
1389
+ responder;
1390
+ violations = [];
1391
+ questions = [];
1392
+ artifacts = [];
1393
+ reports = [];
1394
+ descriptor;
1395
+ completionSummary;
1396
+ errorMessage;
1397
+ constructor(ids, options = {}) {
1398
+ this.ids = ids;
1399
+ this.responder = options.responder;
1400
+ this.decoder = new ProtocolDecoder(options.maxBytes);
1401
+ }
1402
+ /** The v2 messages OmniNode sends to open a task (written to the agent's stdin). */
1403
+ taskMessages(objective, role, context, instruction) {
1404
+ const ids = this.ids;
1405
+ const messages = [envelope("TASK", { taskId: ids.taskId, objective }, ids)];
1406
+ if (role !== void 0) messages.push(envelope("CONTEXT", { role }, ids));
1407
+ if (context !== void 0 && context.length > 0) messages.push(envelope("CONTEXT", { body: context }, ids));
1408
+ if (instruction !== void 0 && instruction.length > 0) messages.push(envelope("INSTRUCTION", { text: instruction }, ids));
1409
+ return messages.map((m) => encodeMessage(m));
1410
+ }
1411
+ helloAck(descriptor) {
1412
+ return encodeMessage(buildHelloAck(descriptor));
1413
+ }
1414
+ /** Consumes decoded stdout, answering questions as they arrive. */
1415
+ async handle(decoded) {
1416
+ if (!decoded.ok) {
1417
+ this.violations.push(decoded.error);
1418
+ return;
1419
+ }
1420
+ const message = decoded.envelope;
1421
+ if (message.protocol === PROTOCOL_V2) this.sawV2 = true;
1422
+ if (message.type === "HELLO") {
1423
+ const hello = parseHello(message);
1424
+ if (hello.ok) this.descriptor = hello.descriptor;
1425
+ else this.violations.push({ code: "INVALID_PAYLOAD", message: `bad HELLO: ${hello.reason}` });
1426
+ return;
1427
+ }
1428
+ if (message.type === "QUESTION") {
1429
+ await this.answerQuestion(message);
1430
+ return;
1431
+ }
1432
+ if (message.type === "REPORT") {
1433
+ this.reports.push(message.payload);
1434
+ return;
1435
+ }
1436
+ if (message.type === "ARTIFACT") {
1437
+ this.artifacts.push(message.payload);
1438
+ return;
1439
+ }
1440
+ if (message.type === "COMPLETION") {
1441
+ const payload = message.payload;
1442
+ this.completionSummary = typeof payload?.summary === "string" ? payload.summary : "";
1443
+ return;
1444
+ }
1445
+ if (message.type === "ERROR") {
1446
+ const payload = message.payload;
1447
+ const detail = typeof payload?.message === "string" ? payload.message : "agent reported an error";
1448
+ const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
1449
+ this.errorMessage = `Agent error${code}: ${detail}`;
1450
+ return;
1451
+ }
1452
+ if (message.type === "TASK_ACCEPTED" || message.type === "TASK_STARTED" || message.type === "STATUS") {
1453
+ return;
1454
+ }
1455
+ this.violations.push({
1456
+ code: "UNKNOWN_TYPE",
1457
+ message: `unexpected message type ${message.type} from agent`
1458
+ });
1459
+ }
1460
+ async answerQuestion(message) {
1461
+ const payload = message.payload ?? {};
1462
+ const questionId = typeof payload.questionId === "string" ? payload.questionId : message.messageId;
1463
+ const question = typeof payload.question === "string" ? payload.question : "";
1464
+ const expected = typeof payload.expected === "string" ? payload.expected : "text";
1465
+ let answer;
1466
+ if (this.responder) {
1467
+ try {
1468
+ answer = await this.responder({ questionId, question, expected });
1469
+ } catch (error) {
1470
+ this.violations.push({
1471
+ code: "INVALID_PAYLOAD",
1472
+ message: `question responder failed: ${error instanceof Error ? error.message : String(error)}`
1473
+ });
1474
+ }
1475
+ }
1476
+ this.questions.push({ questionId, question, ...answer !== void 0 ? { answer } : {} });
1477
+ this.pendingAnswers.push(
1478
+ encodeMessage(
1479
+ envelope("RESPONSE", { questionId, ...answer !== void 0 ? { body: answer } : {} }, {
1480
+ ...this.ids,
1481
+ requestId: message.requestId ?? this.ids.requestId
1482
+ })
1483
+ )
1484
+ );
1485
+ }
1486
+ pendingAnswers = [];
1487
+ /** True once any inbound message carried the v2 protocol field. */
1488
+ sawV2 = false;
1489
+ };
1490
+
1491
+ // src/agents/cwd.ts
1492
+ import { statSync } from "fs";
1493
+ import { isAbsolute, relative, resolve } from "path";
1494
+ function validateWorkingDirectory(cwd, options = {}) {
1495
+ if (!cwd) return void 0;
1496
+ const normalized = isAbsolute(cwd) ? resolve(cwd) : resolve(process.cwd(), cwd);
1497
+ let stats;
1498
+ try {
1499
+ stats = statSync(normalized);
1500
+ } catch (error) {
1501
+ throw new AgentError(
1502
+ "AGENT_NOT_FOUND",
1503
+ `Working directory "${normalized}" does not exist or is not accessible.`,
1504
+ { cause: error, details: { cwd: normalized } }
1505
+ );
1506
+ }
1507
+ if (!stats.isDirectory()) {
1508
+ throw new AgentError("AGENT_NOT_FOUND", `Working directory "${normalized}" is not a directory.`, {
1509
+ details: { cwd: normalized }
1510
+ });
1511
+ }
1512
+ try {
1513
+ statSync(resolve(normalized, "."));
1514
+ } catch (error) {
1515
+ throw new AgentError(
1516
+ "AGENT_NOT_FOUND",
1517
+ `Working directory "${normalized}" is not accessible.`,
1518
+ { cause: error }
1519
+ );
1520
+ }
1521
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
1522
+ const rel = relative(projectRoot, normalized);
1523
+ const outsideProject = rel.startsWith("..") || isAbsolute(rel);
1524
+ if (outsideProject && options.allowExternal !== true) {
1525
+ throw new AgentError(
1526
+ "AGENT_NOT_FOUND",
1527
+ `Working directory "${normalized}" is outside the project root "${projectRoot}". Set allow_external_cwd: true on the agent to permit this.`,
1528
+ { details: { cwd: normalized, projectRoot } }
1529
+ );
1530
+ }
1531
+ return { cwd: normalized, outsideProject };
1532
+ }
1533
+
1534
+ // src/agents/env.ts
1535
+ var BASELINE_KEYS = ["PATH", "HOME"];
1536
+ function resolveEnvPolicy(config) {
1537
+ if (config.envPolicy !== void 0) return config.envPolicy;
1538
+ if (config.inheritEnv === false) return "explicit";
1539
+ return "inherit";
1540
+ }
902
1541
  function buildChildEnv(config) {
903
1542
  const configured = config.env ?? {};
904
- if (config.inheritEnv !== false) {
1543
+ const policy = resolveEnvPolicy(config);
1544
+ if (policy === "inherit") {
905
1545
  return { ...process.env, ...configured };
906
1546
  }
907
- return {
908
- PATH: process.env.PATH ?? "",
909
- HOME: process.env.HOME ?? "",
910
- ...configured
911
- };
1547
+ const base = {};
1548
+ for (const key of BASELINE_KEYS) {
1549
+ const value = process.env[key];
1550
+ if (value !== void 0) base[key] = value;
1551
+ }
1552
+ switch (policy) {
1553
+ case "explicit":
1554
+ return { ...base, ...configured };
1555
+ case "allowlist": {
1556
+ const allowed = { ...base };
1557
+ for (const key of config.envAllowlist ?? []) {
1558
+ const value = process.env[key];
1559
+ if (value !== void 0) allowed[key] = value;
1560
+ }
1561
+ return { ...allowed, ...configured };
1562
+ }
1563
+ case "denylist": {
1564
+ const denied = new Set((config.envDenylist ?? []).map((k) => k.toUpperCase()));
1565
+ const filtered = {};
1566
+ for (const [key, value] of Object.entries(process.env)) {
1567
+ if (denied.has(key.toUpperCase())) continue;
1568
+ filtered[key] = value;
1569
+ }
1570
+ return { ...filtered, ...configured };
1571
+ }
1572
+ }
912
1573
  }
1574
+
1575
+ // src/agents/process/index.ts
1576
+ var DEFAULT_TIMEOUT_MS2 = 6e5;
1577
+ var KILL_GRACE_MS = 5e3;
1578
+ var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
913
1579
  function composeTaskText(input) {
914
1580
  const sections = [`# Objective
915
1581
  ${input.objective}`];
@@ -930,18 +1596,19 @@ ${input.instruction}`);
930
1596
  return sections.join("\n\n");
931
1597
  }
932
1598
  function buildProtocolMessages(input, agentName) {
933
- const meta = {
1599
+ const session = new ProtocolSession({
1600
+ requestId: `req-${input.taskId}`,
934
1601
  taskId: input.taskId,
935
- agent: agentName,
936
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
937
- };
938
- const messages = [
939
- { type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
940
- ];
941
- if (input.role) messages.push({ type: "ROLE", payload: input.role });
942
- if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
943
- if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
944
- return messages.map((message) => JSON.stringify({ meta, message }));
1602
+ agentId: agentName,
1603
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1604
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1605
+ });
1606
+ return session.taskMessages(
1607
+ input.objective,
1608
+ input.role,
1609
+ input.context,
1610
+ input.instruction
1611
+ );
945
1612
  }
946
1613
  var ProcessAgent = class {
947
1614
  config;
@@ -970,40 +1637,73 @@ var ProcessAgent = class {
970
1637
  }
971
1638
  const mode = this.config.inputMode ?? "stdin";
972
1639
  const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
973
- return new Promise((resolve, reject) => {
1640
+ return new Promise((resolve2, reject) => {
1641
+ let cwd;
1642
+ try {
1643
+ cwd = validateWorkingDirectory(this.config.cwd, {
1644
+ allowExternal: this.config.allowExternalCwd
1645
+ })?.cwd;
1646
+ } catch (error) {
1647
+ reject(error);
1648
+ return;
1649
+ }
974
1650
  const child = spawn(
975
1651
  command,
976
1652
  [...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
977
1653
  {
978
- cwd: this.config.cwd,
1654
+ ...cwd !== void 0 ? { cwd } : {},
979
1655
  env: buildChildEnv(this.config),
980
- stdio: ["pipe", "pipe", "pipe"]
1656
+ stdio: ["pipe", "pipe", "pipe"],
1657
+ // Own process group so the whole tree can be terminated together
1658
+ // (roadmap §8: no orphaned children).
1659
+ ...process.platform === "win32" ? {} : { detached: true }
981
1660
  }
982
1661
  );
983
1662
  this.running.set(input.taskId, child);
1663
+ GLOBAL_RUNNING.add(child);
1664
+ installSignalForwarding();
984
1665
  let stdout = "";
985
1666
  let stderr = "";
986
1667
  let timedOut = false;
1668
+ let outputLimitExceeded = false;
987
1669
  let settled = false;
1670
+ const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
988
1671
  const timer = setTimeout(() => {
989
1672
  timedOut = true;
990
- this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
991
- child.kill("SIGTERM");
992
- setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
1673
+ this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process tree.`);
1674
+ killTree(child, "SIGTERM");
1675
+ setTimeout(() => killTree(child, "SIGKILL"), KILL_GRACE_MS).unref();
993
1676
  }, timeoutMs);
994
1677
  const settle = (finish) => {
995
1678
  if (settled) return;
996
1679
  settled = true;
997
1680
  clearTimeout(timer);
998
1681
  this.running.delete(input.taskId);
1682
+ GLOBAL_RUNNING.delete(child);
999
1683
  finish();
1000
1684
  };
1001
- child.stdout?.on("data", (chunk) => {
1002
- stdout += chunk.toString();
1003
- });
1004
- child.stderr?.on("data", (chunk) => {
1005
- stderr += chunk.toString();
1006
- });
1685
+ const collect = (into, chunk) => {
1686
+ if (outputLimitExceeded) return;
1687
+ const text = sanitizeChunk(chunk);
1688
+ if (into === "stdout") {
1689
+ if (stdout.length + text.length > maxOutputBytes) {
1690
+ stdout = (stdout + text).slice(0, maxOutputBytes);
1691
+ outputLimitExceeded = true;
1692
+ this.log.warn(`Agent "${this.config.name}" exceeded the ${maxOutputBytes} byte output limit \u2014 killing it.`);
1693
+ killTree(child, "SIGKILL");
1694
+ return;
1695
+ }
1696
+ stdout += text;
1697
+ } else {
1698
+ if (stderr.length + text.length > maxOutputBytes) {
1699
+ stderr = (stderr + text).slice(0, maxOutputBytes);
1700
+ return;
1701
+ }
1702
+ stderr += text;
1703
+ }
1704
+ };
1705
+ child.stdout?.on("data", (chunk) => collect("stdout", chunk));
1706
+ child.stderr?.on("data", (chunk) => collect("stderr", chunk));
1007
1707
  child.stdin?.on("error", () => {
1008
1708
  });
1009
1709
  if (mode !== "arg") {
@@ -1037,7 +1737,19 @@ var ProcessAgent = class {
1037
1737
  new AgentError(
1038
1738
  "AGENT_TIMEOUT",
1039
1739
  `Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
1040
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 4e3), stderr: stderr.slice(0, 2e3) } }
1740
+ {
1741
+ details: {
1742
+ operation: "agent.run",
1743
+ agent: this.config.name,
1744
+ durationMs: timeoutMs,
1745
+ // The process was started and may have performed side
1746
+ // effects before being killed — the outcome is unknown,
1747
+ // not failed (roadmap §6.3).
1748
+ dispatched: true,
1749
+ stdout: stdout.slice(0, 4e3),
1750
+ stderr: stderr.slice(0, 2e3)
1751
+ }
1752
+ }
1041
1753
  )
1042
1754
  );
1043
1755
  return;
@@ -1045,7 +1757,7 @@ var ProcessAgent = class {
1045
1757
  const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
1046
1758
  if (mode === "protocol") {
1047
1759
  try {
1048
- resolve(
1760
+ resolve2(
1049
1761
  interpretProtocolRun({
1050
1762
  input,
1051
1763
  agentName: this.config.name,
@@ -1060,14 +1772,14 @@ var ProcessAgent = class {
1060
1772
  return;
1061
1773
  }
1062
1774
  const trimmed = stdout.trim();
1063
- resolve({
1775
+ resolve2({
1064
1776
  taskId: input.taskId,
1065
- status: code === 0 ? "completed" : "failed",
1777
+ status: code === 0 && !outputLimitExceeded ? "completed" : "failed",
1066
1778
  summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
1067
1779
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
1068
1780
  exitCode: code ?? void 0,
1069
1781
  logs,
1070
- error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
1782
+ ...outputLimitExceeded ? { error: `Agent exceeded the ${maxOutputBytes} byte output limit and was terminated.` } : code === 0 ? {} : { error: describeExit(code, signal) }
1071
1783
  });
1072
1784
  });
1073
1785
  });
@@ -1076,67 +1788,117 @@ var ProcessAgent = class {
1076
1788
  /** Ask the agent to stop current work (SIGTERM). */
1077
1789
  async cancel(taskId) {
1078
1790
  const child = this.running.get(taskId);
1079
- if (child) child.kill("SIGTERM");
1791
+ if (child) killTree(child, "SIGTERM");
1080
1792
  }
1081
1793
  runningTaskIds() {
1082
1794
  return [...this.running.keys()];
1083
1795
  }
1084
1796
  };
1085
- function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
1086
- const reports = [];
1087
- let summary;
1088
- let error;
1089
- let sawProtocolMessage = false;
1090
- for (const line of stdout.split("\n")) {
1091
- const trimmed2 = line.trim();
1092
- if (trimmed2.length === 0) continue;
1093
- const message = parseAgentMessage(trimmed2);
1094
- if (!message) continue;
1095
- sawProtocolMessage = true;
1096
- if (message.type === "REPORT") {
1097
- const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
1098
- if (report) reports.push(report);
1099
- } else if (message.type === "COMPLETION") {
1100
- const payload = message.payload;
1101
- if (typeof payload?.summary === "string") summary = payload.summary;
1102
- } else if (message.type === "ERROR") {
1103
- const payload = message.payload;
1104
- const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
1105
- const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
1106
- error = `Agent error${code}: ${detail}`;
1107
- }
1797
+ function interpretProtocolRun({
1798
+ input,
1799
+ agentName,
1800
+ stdout,
1801
+ exitCode,
1802
+ logs,
1803
+ responder
1804
+ }) {
1805
+ const session = new ProtocolSession(
1806
+ {
1807
+ requestId: `req-${input.taskId}`,
1808
+ taskId: input.taskId,
1809
+ agentId: agentName,
1810
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1811
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1812
+ },
1813
+ { ...responder !== void 0 ? { responder } : {} }
1814
+ );
1815
+ for (const result of session.decoder.push(stdout)) {
1816
+ session.handle(result);
1108
1817
  }
1109
- if (!sawProtocolMessage && exitCode === 0) {
1110
- throw new ProtocolError(
1111
- `Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
1112
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
1113
- );
1818
+ for (const result of session.decoder.flush()) {
1819
+ session.handle(result);
1114
1820
  }
1821
+ 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);
1115
1822
  const trimmed = stdout.trim();
1823
+ const protocolBlock = {
1824
+ protocol: session.descriptor?.protocol ?? PROTOCOL_V2,
1825
+ legacy: !session.sawV2 && session.descriptor === void 0,
1826
+ ...session.descriptor !== void 0 ? { descriptor: session.descriptor } : {},
1827
+ questions: session.questions,
1828
+ violations: session.violations.map((v) => `${v.code}: ${v.message}`)
1829
+ };
1830
+ if (session.errorMessage !== void 0) {
1831
+ return {
1832
+ taskId: input.taskId,
1833
+ status: "failed",
1834
+ error: session.errorMessage,
1835
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1836
+ exitCode: exitCode ?? void 0,
1837
+ logs,
1838
+ protocol: protocolBlock,
1839
+ ...reports.length > 0 ? { reports } : {}
1840
+ };
1841
+ }
1842
+ if (session.descriptor === void 0 && session.violations.length > 0 && session.reports.length === 0 && session.completionSummary === void 0) {
1843
+ return {
1844
+ taskId: input.taskId,
1845
+ status: "failed",
1846
+ error: `agent produced no valid protocol messages: ${protocolBlock.violations.join("; ")}`,
1847
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1848
+ exitCode: exitCode ?? void 0,
1849
+ logs,
1850
+ protocol: protocolBlock
1851
+ };
1852
+ }
1853
+ const completed = exitCode === 0 && session.completionSummary !== void 0;
1116
1854
  return {
1117
1855
  taskId: input.taskId,
1118
- status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
1119
- summary: summary ?? reports[0]?.summary,
1856
+ status: completed || exitCode === 0 ? completed ? "completed" : "completed" : "failed",
1857
+ summary: session.completionSummary ?? reports[0]?.summary,
1120
1858
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
1121
1859
  exitCode: exitCode ?? void 0,
1122
1860
  logs,
1123
- reports: reports.length > 0 ? reports : void 0,
1124
- error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
1861
+ protocol: protocolBlock,
1862
+ ...reports.length > 0 ? { reports } : {},
1863
+ ...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
1125
1864
  };
1126
1865
  }
1127
- function parseAgentMessage(line) {
1128
- try {
1129
- const parsed = JSON.parse(line);
1130
- if (typeof parsed.type === "string") {
1131
- return { type: parsed.type, payload: parsed.payload };
1132
- }
1133
- const inner = parsed.message;
1134
- if (inner && typeof inner.type === "string") {
1135
- return { type: inner.type, payload: inner.payload };
1866
+ var GLOBAL_RUNNING = /* @__PURE__ */ new Set();
1867
+ var signalForwardingInstalled = false;
1868
+ function installSignalForwarding() {
1869
+ if (signalForwardingInstalled) return;
1870
+ signalForwardingInstalled = true;
1871
+ for (const signal of ["SIGINT", "SIGTERM"]) {
1872
+ process.on(signal, () => {
1873
+ for (const child of GLOBAL_RUNNING) {
1874
+ killTree(child, "SIGTERM");
1875
+ }
1876
+ });
1877
+ }
1878
+ }
1879
+ function killTree(child, signal) {
1880
+ if (child.pid === void 0) return;
1881
+ if (process.platform !== "win32") {
1882
+ try {
1883
+ process.kill(-child.pid, signal);
1884
+ return;
1885
+ } catch {
1136
1886
  }
1887
+ }
1888
+ try {
1889
+ child.kill(signal);
1137
1890
  } catch {
1138
1891
  }
1139
- return void 0;
1892
+ }
1893
+ function describeExit(code, signal) {
1894
+ if (signal !== null) {
1895
+ return signal === "SIGKILL" ? "Process was killed (SIGKILL) \u2014 usually an external kill or OOM." : `Process terminated by signal ${signal}.`;
1896
+ }
1897
+ if (code === null) return "Process terminated without an exit code.";
1898
+ return `Process exited with code ${code}.`;
1899
+ }
1900
+ function sanitizeChunk(chunk) {
1901
+ return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
1140
1902
  }
1141
1903
  function normalizeReport(payload, taskId, agentName, index) {
1142
1904
  if (payload === null || typeof payload !== "object") return void 0;
@@ -1157,6 +1919,40 @@ function firstLine(text, maxLength = 200) {
1157
1919
  return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
1158
1920
  }
1159
1921
 
1922
+ // src/agents/adapters.ts
1923
+ var AgentAdapterRegistry = class {
1924
+ adapters = /* @__PURE__ */ new Map();
1925
+ register(adapter) {
1926
+ this.adapters.set(adapter.integration, adapter);
1927
+ }
1928
+ get(integration) {
1929
+ return this.adapters.get(integration);
1930
+ }
1931
+ list() {
1932
+ return [...this.adapters.values()];
1933
+ }
1934
+ create(config) {
1935
+ const adapter = this.get(config.integration);
1936
+ if (!adapter) {
1937
+ throw new AgentError(
1938
+ "AGENT_NOT_FOUND",
1939
+ `No agent adapter registered for integration "${config.integration}". Registered: ${[...this.adapters.keys()].join(", ") || "(none)"}.`
1940
+ );
1941
+ }
1942
+ return adapter.create(config);
1943
+ }
1944
+ };
1945
+ var ProcessAgentAdapter = class {
1946
+ constructor(factory) {
1947
+ this.factory = factory;
1948
+ }
1949
+ factory;
1950
+ integration = "process";
1951
+ create(config) {
1952
+ return this.factory(config);
1953
+ }
1954
+ };
1955
+
1160
1956
  // src/agents/registry.ts
1161
1957
  var AgentRegistry = class {
1162
1958
  agents = /* @__PURE__ */ new Map();
@@ -1172,14 +1968,15 @@ var AgentRegistry = class {
1172
1968
  };
1173
1969
 
1174
1970
  // src/agents/factory.ts
1175
- function createAgent(config) {
1176
- if (config.integration === "process") {
1177
- return new ProcessAgent(config);
1178
- }
1971
+ var defaultAgentAdapters = new AgentAdapterRegistry();
1972
+ defaultAgentAdapters.register(new ProcessAgentAdapter((config) => new ProcessAgent(config)));
1973
+ function createAgent(config, adapters = defaultAgentAdapters) {
1974
+ const registered = adapters.get(config.integration);
1975
+ if (registered) return registered.create(config);
1179
1976
  if (config.integration === "native") {
1180
1977
  throw new OmniNodeError(
1181
1978
  "NOT_IMPLEMENTED",
1182
- `Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
1979
+ `No native adapter is registered for "${config.name}". Register an IAgentAdapter (createAgent(config, adapters)) or use input over stdin/stdout (type "cli").`
1183
1980
  );
1184
1981
  }
1185
1982
  throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
@@ -1222,7 +2019,7 @@ function registerAgentCommands(program) {
1222
2019
  "--input-mode <mode>",
1223
2020
  "How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
1224
2021
  "stdin"
1225
- ).option(
2022
+ ).option("--env-policy <policy>", "Environment policy: inherit, allowlist, denylist or explicit.").option("--allow-env <name>", "Variable to forward under the allowlist policy (repeatable).", collectArgs).option("--deny-env <name>", "Variable to remove under the denylist policy (repeatable).", collectArgs).option("--max-output-bytes <n>", "Per-stream output budget in bytes (default 5 MiB).", (value) => Number(value)).option("--allow-external-cwd", "Allow a working directory outside the project root.").option(
1226
2023
  "--timeout-ms <ms>",
1227
2024
  "Per-task timeout in milliseconds.",
1228
2025
  (value) => Number(value)
@@ -1237,7 +2034,12 @@ async function addAgent(options) {
1237
2034
  ...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
1238
2035
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1239
2036
  input_mode: options.inputMode,
1240
- ...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {}
2037
+ ...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
2038
+ ...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
2039
+ ...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
2040
+ ...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
2041
+ ...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
2042
+ ...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
1241
2043
  });
1242
2044
  if (!candidate.success) {
1243
2045
  const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
@@ -1397,12 +2199,18 @@ import { randomBytes } from "crypto";
1397
2199
  var ALLOWED_TRANSITIONS = {
1398
2200
  created: ["queued", "running", "cancelled"],
1399
2201
  queued: ["running", "cancelled"],
1400
- running: ["completed", "failed", "cancelled"],
2202
+ running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
1401
2203
  waiting: ["running", "cancelled"],
1402
2204
  completed: [],
1403
- failed: [],
1404
- cancelled: []
2205
+ // failed → running is the only re-entry: bounded automatic retry (§6.4).
2206
+ failed: ["running"],
2207
+ timed_out: [],
2208
+ cancelled: [],
2209
+ unknown: [],
2210
+ partially_completed: []
1405
2211
  };
2212
+ var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
2213
+ var EXECUTION_HISTORY_LIMIT = 10;
1406
2214
  var TaskEngine = class {
1407
2215
  constructor(options) {
1408
2216
  this.options = options;
@@ -1434,6 +2242,7 @@ var TaskEngine = class {
1434
2242
  id: generateTaskId(),
1435
2243
  objective: input.objective.trim(),
1436
2244
  status: "created",
2245
+ attempt: 0,
1437
2246
  createdAt: now,
1438
2247
  updatedAt: now,
1439
2248
  ...input.project !== void 0 ? { project: input.project } : {},
@@ -1450,8 +2259,10 @@ var TaskEngine = class {
1450
2259
  const task = await this.mustGet(id);
1451
2260
  return this.transition(task, "queued");
1452
2261
  }
1453
- /** Runs the task through its assigned agent and records the result. */
1454
- async run(id) {
2262
+ /** Runs the task through its assigned agent and records the result.
2263
+ * `maxAttempts` enables bounded automatic retries of *retryable* errors
2264
+ * with linear backoff; unknown/timed_out outcomes are never auto-retried. */
2265
+ async run(id, options = {}) {
1455
2266
  const task = await this.mustGet(id);
1456
2267
  if (task.status !== "created" && task.status !== "queued") {
1457
2268
  throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
@@ -1464,49 +2275,89 @@ var TaskEngine = class {
1464
2275
  throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
1465
2276
  }
1466
2277
  const role = task.role ? this.options.roles.get(task.role) : void 0;
1467
- const running = await this.transition(task, "running");
1468
2278
  this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1469
- let background = running.context?.background;
2279
+ let background = task.context?.background;
1470
2280
  if (this.options.memory) {
1471
2281
  const memoryContext = await this.options.memory.gatherContext({
1472
- objective: running.objective,
1473
- ...running.role ? { role: running.role } : {},
1474
- ...running.project ? { project: running.project } : {}
2282
+ objective: task.objective,
2283
+ ...task.role ? { role: task.role } : {},
2284
+ ...task.project ? { project: task.project } : {}
1475
2285
  });
1476
2286
  if (memoryContext.length > 0) {
1477
2287
  background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
1478
2288
  }
1479
2289
  }
1480
- let output;
1481
- try {
1482
- output = await agent.run({
1483
- taskId: running.id,
1484
- objective: running.objective,
1485
- ...role ? { role } : {},
1486
- ...background !== void 0 ? { context: background } : {}
1487
- });
1488
- } catch (error) {
1489
- const message = error instanceof Error ? error.message : String(error);
1490
- this.log.error(`Task ${task.id} failed: ${message}`);
1491
- return this.transition(running, "failed", {
1492
- result: { error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }
2290
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
2291
+ const backoffMs = options.retryBackoffMs ?? 0;
2292
+ let current = task;
2293
+ for (let i = 0; i < maxAttempts; i += 1) {
2294
+ const attempt = current.attempt + 1;
2295
+ const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
2296
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2297
+ current = await this.transition(current, "running", { attempt });
2298
+ let output;
2299
+ let thrown;
2300
+ try {
2301
+ output = await agent.run({
2302
+ taskId: current.id,
2303
+ objective: current.objective,
2304
+ ...role ? { role } : {},
2305
+ ...background !== void 0 ? { context: background } : {}
2306
+ });
2307
+ } catch (error) {
2308
+ thrown = error;
2309
+ }
2310
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2311
+ if (thrown !== void 0 || output === void 0) {
2312
+ const outcome = classifyThrown(thrown);
2313
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
2314
+ const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
2315
+ current = await this.transition(current, outcome, {
2316
+ attempt,
2317
+ lastError: message,
2318
+ execution: {
2319
+ executionId,
2320
+ attempt,
2321
+ startedAt,
2322
+ finishedAt,
2323
+ outcome,
2324
+ error: message
2325
+ }
2326
+ });
2327
+ this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
2328
+ if (retryable) {
2329
+ if (backoffMs > 0) await sleep(backoffMs * attempt);
2330
+ continue;
2331
+ }
2332
+ if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
2333
+ return current;
2334
+ }
2335
+ const currentState = await this.options.store.get(current.id);
2336
+ if (currentState?.status === "cancelled") {
2337
+ current = currentState;
2338
+ break;
2339
+ }
2340
+ const result = {
2341
+ ...output.summary !== void 0 ? { summary: output.summary } : {},
2342
+ ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
2343
+ // Keep the verbatim output (bounded) for report extraction and audit.
2344
+ ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
2345
+ ...output.error !== void 0 ? { error: output.error } : {},
2346
+ finishedAt
2347
+ };
2348
+ const finalStatus = output.status === "completed" ? "completed" : "failed";
2349
+ current = await this.transition(current, finalStatus, {
2350
+ attempt,
2351
+ result,
2352
+ execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
1493
2353
  });
2354
+ this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
2355
+ break;
1494
2356
  }
1495
- const result = {
1496
- ...output.summary !== void 0 ? { summary: output.summary } : {},
1497
- ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1498
- // Keep the verbatim output (bounded) for report extraction and audit.
1499
- ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1500
- ...output.error !== void 0 ? { error: output.error } : {},
1501
- finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1502
- };
1503
- const finalStatus = output.status === "completed" ? "completed" : "failed";
1504
- const finished = await this.transition(running, finalStatus, { result });
1505
- this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
1506
2357
  if (this.options.memory) {
1507
- await this.options.memory.recordTaskOutcome(finished);
2358
+ await this.options.memory.recordTaskOutcome(current);
1508
2359
  }
1509
- return finished;
2360
+ return current;
1510
2361
  }
1511
2362
  async cancel(id) {
1512
2363
  const task = await this.mustGet(id);
@@ -1519,13 +2370,14 @@ var TaskEngine = class {
1519
2370
  }
1520
2371
  return this.transition(task, "cancelled");
1521
2372
  }
1522
- /** Error recovery (§27 Phase 9): reset a failed/cancelled task so it can run again. */
2373
+ /** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
1523
2374
  async retry(id) {
1524
2375
  const task = await this.mustGet(id);
1525
- if (task.status !== "failed" && task.status !== "cancelled") {
2376
+ const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
2377
+ if (!retryableStates.includes(task.status)) {
1526
2378
  throw new TaskError(
1527
2379
  "TASK_INVALID",
1528
- `Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
2380
+ `Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1529
2381
  );
1530
2382
  }
1531
2383
  const { result: _discarded, ...rest } = task;
@@ -1557,10 +2409,14 @@ var TaskEngine = class {
1557
2409
  if (!allowed.includes(to)) {
1558
2410
  throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
1559
2411
  }
2412
+ const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
1560
2413
  const updated = {
1561
2414
  ...task,
1562
2415
  status: to,
1563
2416
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2417
+ ...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
2418
+ ...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
2419
+ ...executions !== void 0 ? { executions } : {},
1564
2420
  ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
1565
2421
  };
1566
2422
  await this.options.store.save(updated);
@@ -1581,6 +2437,19 @@ var TaskEngine = class {
1581
2437
  function generateTaskId() {
1582
2438
  return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1583
2439
  }
2440
+ function errorCode(error) {
2441
+ return error?.code ?? "";
2442
+ }
2443
+ function classifyThrown(error) {
2444
+ if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
2445
+ const dispatched = error.details?.dispatched;
2446
+ return dispatched === false ? "timed_out" : "unknown";
2447
+ }
2448
+ return "failed";
2449
+ }
2450
+ function sleep(ms) {
2451
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2452
+ }
1584
2453
 
1585
2454
  // src/audit/index.ts
1586
2455
  import { appendFile, mkdir as mkdir2 } from "fs/promises";
@@ -1623,6 +2492,23 @@ var FileAuditLog = class {
1623
2492
  }
1624
2493
  };
1625
2494
 
2495
+ // src/memory/base.ts
2496
+ function defaultSearch(provider, text, options = {}) {
2497
+ return provider.retrieve({
2498
+ text,
2499
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
2500
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
2501
+ });
2502
+ }
2503
+ function defaultMetadata(name, options = {}) {
2504
+ return {
2505
+ name,
2506
+ capabilities: options.capabilities ?? ["retrieve", "store", "search"],
2507
+ relevanceRanking: options.relevanceRanking ?? false,
2508
+ ...options.backend !== void 0 ? { backend: options.backend } : {}
2509
+ };
2510
+ }
2511
+
1626
2512
  // src/memory/local.ts
1627
2513
  var STOPWORDS = /* @__PURE__ */ new Set([
1628
2514
  "the",
@@ -1685,9 +2571,27 @@ var LocalMemoryProvider = class extends JsonFileStore {
1685
2571
  constructor(directory) {
1686
2572
  super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
1687
2573
  }
1688
- async query(query) {
2574
+ metadata() {
2575
+ return defaultMetadata(this.name, {
2576
+ capabilities: ["retrieve", "store", "search", "categories", "tags"],
2577
+ relevanceRanking: true,
2578
+ backend: { kind: "json-file", deterministicScoring: true }
2579
+ });
2580
+ }
2581
+ search(text, options = {}) {
2582
+ return defaultSearch(this, text, options);
2583
+ }
2584
+ /** @deprecated v1 alias of {@link retrieve}. */
2585
+ query(query) {
2586
+ return this.retrieve(query);
2587
+ }
2588
+ /** @deprecated v1 alias of {@link store}. */
2589
+ write(entry2) {
2590
+ return this.store(entry2);
2591
+ }
2592
+ async retrieve(query) {
1689
2593
  const entries = await this.readAll();
1690
- const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter(
2594
+ const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter((entry2) => query.category ? entry2.category === query.category : true).filter(
1691
2595
  (entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
1692
2596
  );
1693
2597
  if (!query.text || query.text.trim().length === 0) {
@@ -1696,7 +2600,7 @@ var LocalMemoryProvider = class extends JsonFileStore {
1696
2600
  const queryKeywords = keywords(query.text);
1697
2601
  return filtered.map((entry2) => ({ entry: entry2, score: relevanceScore(queryKeywords, entry2) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry: entry2 }) => entry2);
1698
2602
  }
1699
- write(entry2) {
2603
+ store(entry2) {
1700
2604
  return this.mutate((entries) => {
1701
2605
  const index = entries.findIndex((e) => e.key === entry2.key);
1702
2606
  const stamped = {
@@ -1727,11 +2631,40 @@ var RememberaMemoryProvider = class {
1727
2631
  this.env = config.env ?? process.env;
1728
2632
  this.log = log.child({ component: "remembera" });
1729
2633
  }
2634
+ get authenticationConfigured() {
2635
+ return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
2636
+ }
1730
2637
  headers() {
1731
2638
  const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
1732
2639
  return key ? { authorization: `Bearer ${key}` } : {};
1733
2640
  }
1734
- async query(query) {
2641
+ /** Search delegates to the gateway's relevance query. */
2642
+ search(text, options = {}) {
2643
+ return defaultSearch(
2644
+ this,
2645
+ text,
2646
+ {
2647
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
2648
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
2649
+ }
2650
+ );
2651
+ }
2652
+ metadata() {
2653
+ return defaultMetadata(this.name, {
2654
+ capabilities: ["retrieve", "store", "search", "categories", "tags"],
2655
+ relevanceRanking: true,
2656
+ backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
2657
+ });
2658
+ }
2659
+ /** @deprecated v1 alias of {@link retrieve}. */
2660
+ query(query) {
2661
+ return this.retrieve(query);
2662
+ }
2663
+ /** @deprecated v1 alias of {@link store}. */
2664
+ write(entry2) {
2665
+ return this.store(entry2);
2666
+ }
2667
+ async retrieve(query) {
1735
2668
  const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
1736
2669
  if (status !== 200) {
1737
2670
  throw new MemoryError(`Remembera query returned HTTP ${status}.`);
@@ -1742,7 +2675,7 @@ var RememberaMemoryProvider = class {
1742
2675
  }
1743
2676
  return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
1744
2677
  }
1745
- async write(entry2) {
2678
+ async store(entry2) {
1746
2679
  const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
1747
2680
  if (status !== 200 && status !== 201) {
1748
2681
  throw new MemoryError(`Remembera write returned HTTP ${status}.`);
@@ -1777,6 +2710,7 @@ var RememberaMemoryProvider = class {
1777
2710
  content: entry2.content,
1778
2711
  scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
1779
2712
  ...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
2713
+ ...typeof entry2.category === "string" ? { category: entry2.category } : {},
1780
2714
  ...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
1781
2715
  ...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
1782
2716
  };
@@ -1807,57 +2741,54 @@ function createMemoryProvider(config) {
1807
2741
  }
1808
2742
 
1809
2743
  // src/memory/service.ts
2744
+ var DEFAULT_ENTRY_LIMIT = 8;
1810
2745
  var MAX_CONTEXT_CHARS = 4e3;
1811
2746
  var MemoryService = class {
1812
- constructor(provider, log = logger) {
2747
+ constructor(provider, log = logger, options = {}) {
1813
2748
  this.provider = provider;
1814
2749
  this.log = log.child({ component: "memory", provider: provider.name });
2750
+ this.required = options.required ?? false;
2751
+ this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
1815
2752
  }
1816
2753
  provider;
1817
2754
  log;
2755
+ required;
2756
+ maxContextChars;
1818
2757
  get providerRef() {
1819
2758
  return this.provider;
1820
2759
  }
1821
2760
  get providerName() {
1822
2761
  return this.provider.name;
1823
2762
  }
1824
- /** Relevant context for a task objective; empty string when nothing matches. */
1825
- async gatherContext(query) {
2763
+ get isRequired() {
2764
+ return this.required;
2765
+ }
2766
+ /** Structured, budgeted context for a task (§12 — avoid loading all memory). */
2767
+ async contextFor(query) {
2768
+ let entries;
1826
2769
  try {
1827
- const entries = await this.provider.query({
2770
+ entries = await this.provider.retrieve({
1828
2771
  text: query.objective,
1829
2772
  ...query.role ? { tags: [query.role] } : {},
1830
- limit: 8
2773
+ limit: query.limit ?? DEFAULT_ENTRY_LIMIT
1831
2774
  });
1832
- if (entries.length === 0) return "";
1833
- const byScope = /* @__PURE__ */ new Map();
1834
- for (const entry2 of entries) {
1835
- byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
1836
- }
1837
- const parts = [];
1838
- for (const scope of ["project", "task", "role", "knowledge"]) {
1839
- const scopeEntries = byScope.get(scope) ?? [];
1840
- if (scopeEntries.length === 0) continue;
1841
- const lines = scopeEntries.map((entry2) => `- ${entry2.content}`).join("\n");
1842
- parts.push(`### Relevant memory (${scope})
1843
- ${lines}`);
1844
- }
1845
- const context = parts.join("\n\n");
1846
- return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
1847
2775
  } catch (error) {
1848
- this.log.warn(
1849
- `Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
1850
- );
1851
- return "";
2776
+ return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
1852
2777
  }
2778
+ return this.format(entries);
2779
+ }
2780
+ /** Prompt-ready context text (v1 convenience wrapper). */
2781
+ async gatherContext(query) {
2782
+ return (await this.contextFor(query)).text;
1853
2783
  }
1854
- /** Records a finished task's outcome — best-effort. */
2784
+ /** Records a finished task's outcome — task history plus "known problems" (§12). */
1855
2785
  async recordTaskOutcome(task) {
1856
2786
  const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
1857
2787
  try {
1858
- await this.provider.write({
2788
+ await this.provider.store({
1859
2789
  key: `task:${task.id}`,
1860
2790
  scope: "task",
2791
+ category: "tasks",
1861
2792
  content: `Objective: ${task.objective}
1862
2793
  Status: ${task.status}
1863
2794
  Outcome: ${outcome}`,
@@ -1868,18 +2799,53 @@ Outcome: ${outcome}`,
1868
2799
  (report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
1869
2800
  );
1870
2801
  for (const { report, finding } of significant) {
1871
- await this.provider.write({
2802
+ await this.provider.store({
1872
2803
  key: `finding:${report.id}:${finding.id}`,
1873
2804
  scope: "project",
2805
+ category: "problems",
1874
2806
  content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
1875
2807
  tags: ["known-problem", report.agent]
1876
2808
  });
1877
2809
  }
1878
2810
  } catch (error) {
1879
- this.log.warn(
1880
- `Memory write failed (continuing): ${error instanceof Error ? error.message : String(error)}`
2811
+ this.handleFailure("store", error, void 0);
2812
+ }
2813
+ }
2814
+ format(entries) {
2815
+ if (entries.length === 0) return { text: "", entries, truncated: false };
2816
+ const byScope = /* @__PURE__ */ new Map();
2817
+ for (const entry2 of entries) {
2818
+ byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
2819
+ }
2820
+ const parts = [];
2821
+ for (const scope of ["project", "task", "role", "knowledge"]) {
2822
+ const scopeEntries = byScope.get(scope) ?? [];
2823
+ if (scopeEntries.length === 0) continue;
2824
+ const lines = scopeEntries.map((entry2) => {
2825
+ const category = entry2.category ? `[${entry2.category}] ` : "";
2826
+ return `- ${category}${entry2.content}`;
2827
+ }).join("\n");
2828
+ parts.push(`### Relevant memory (${scope})
2829
+ ${lines}`);
2830
+ }
2831
+ let text = parts.join("\n\n");
2832
+ const truncated = text.length > this.maxContextChars;
2833
+ if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
2834
+ return { text, entries, truncated };
2835
+ }
2836
+ /**
2837
+ * Failure policy (§12): continue unless memory is explicitly required.
2838
+ */
2839
+ handleFailure(operation, error, fallback) {
2840
+ const message = error instanceof Error ? error.message : String(error);
2841
+ if (this.required) {
2842
+ throw new MemoryError(
2843
+ `Memory is marked required, but ${operation} failed: ${message}`,
2844
+ { cause: error }
1881
2845
  );
1882
2846
  }
2847
+ this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
2848
+ return fallback ?? { text: "", entries: [], truncated: false };
1883
2849
  }
1884
2850
  };
1885
2851
 
@@ -1913,7 +2879,11 @@ function createTaskEngine(config, log = logger, directory) {
1913
2879
  agents,
1914
2880
  roles,
1915
2881
  store: new FileTaskStore(directory),
1916
- ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
2882
+ ...config.project.memory !== void 0 ? {
2883
+ memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
2884
+ required: config.project.memory.required
2885
+ })
2886
+ } : {},
1917
2887
  audit: new FileAuditLog(),
1918
2888
  log
1919
2889
  });
@@ -1980,7 +2950,7 @@ function registerTaskCommands(program) {
1980
2950
  const cancelled = await engine.cancel(taskId);
1981
2951
  console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
1982
2952
  });
1983
- task.command("retry <task-id>").description("Reset a failed/cancelled task to created; combine with --run to execute it again.").option("--run", "Run the task again immediately after resetting it.").action(async (taskId, options) => {
2953
+ task.command("retry <task-id>").description("Reset a failed, timed out, unknown or cancelled task to created; combine with --run to execute it again.").option("--run", "Run the task again immediately after resetting it.").action(async (taskId, options) => {
1984
2954
  const config = loadConfig();
1985
2955
  const engine = createTaskEngine(config);
1986
2956
  const reset = await engine.retry(taskId);
@@ -2003,6 +2973,15 @@ function printOutcome(task, options = {}) {
2003
2973
  if (task.context?.files) console.log(` files: ${task.context.files.join(", ")}`);
2004
2974
  console.log(` objective: ${task.objective}`);
2005
2975
  }
2976
+ if (options.verbose && task.attempt > 0) {
2977
+ console.log(` attempt: ${task.attempt}`);
2978
+ }
2979
+ if (task.lastError) console.log(` error: ${task.lastError}`);
2980
+ if (options.verbose && task.executions && task.executions.length > 0) {
2981
+ console.log(
2982
+ ` executions: ${task.executions.map((e) => `#${e.attempt}:${e.outcome}`).join(", ")}`
2983
+ );
2984
+ }
2006
2985
  const result = task.result;
2007
2986
  if (!result) return;
2008
2987
  if (result.summary) console.log(` summary: ${result.summary}`);
@@ -2018,6 +2997,87 @@ function truncate2(text, max) {
2018
2997
 
2019
2998
  // src/reports/aggregate.ts
2020
2999
  import { randomBytes as randomBytes2 } from "crypto";
3000
+
3001
+ // src/reports/schema.ts
3002
+ import { z as z2 } from "zod";
3003
+ var evidenceSchema = z2.union([
3004
+ z2.string().transform((excerpt) => ({ kind: "observation", excerpt })),
3005
+ z2.object({
3006
+ kind: z2.enum(["file", "line", "url", "command", "observation", "artifact"]),
3007
+ ref: z2.string().optional(),
3008
+ line: z2.number().int().optional(),
3009
+ excerpt: z2.string().optional(),
3010
+ agent: z2.string().optional()
3011
+ })
3012
+ ]);
3013
+ var findingSchema = z2.object({
3014
+ id: z2.string().min(1),
3015
+ title: z2.string().min(1),
3016
+ detail: z2.string().default(""),
3017
+ description: z2.string().optional(),
3018
+ severity: z2.enum(["info", "low", "medium", "high", "critical"]).optional(),
3019
+ confidence: z2.enum(["low", "medium", "high"]).optional(),
3020
+ severityBySource: z2.record(z2.enum(["info", "low", "medium", "high", "critical"])).optional(),
3021
+ confidenceBySource: z2.record(z2.enum(["low", "medium", "high"])).optional(),
3022
+ recommendations: z2.array(z2.string()).optional(),
3023
+ evidence: z2.array(evidenceSchema).optional(),
3024
+ location: z2.string().optional(),
3025
+ tags: z2.array(z2.string()).optional(),
3026
+ source: z2.object({ agent: z2.string().optional(), reportId: z2.string().optional(), pipelineRunId: z2.string().optional() }).optional(),
3027
+ recommendation: z2.string().optional()
3028
+ });
3029
+ var artifactSchema = z2.object({
3030
+ kind: z2.enum(["file", "patch", "log", "screenshot", "document", "data"]),
3031
+ ref: z2.string().min(1),
3032
+ description: z2.string().optional(),
3033
+ mediaType: z2.string().optional(),
3034
+ bytes: z2.number().int().optional()
3035
+ });
3036
+ var reportSchema = z2.object({
3037
+ id: z2.string().min(1),
3038
+ taskId: z2.string().min(1),
3039
+ agent: z2.string().min(1),
3040
+ agentId: z2.string().optional(),
3041
+ pipelineRunId: z2.string().optional(),
3042
+ model: z2.string().optional(),
3043
+ summary: z2.string(),
3044
+ findings: z2.array(findingSchema),
3045
+ recommendations: z2.array(z2.string()),
3046
+ confidence: z2.enum(["low", "medium", "high"]).optional(),
3047
+ evidence: z2.array(evidenceSchema).optional(),
3048
+ artifacts: z2.array(artifactSchema).optional(),
3049
+ createdAt: z2.string().min(1),
3050
+ metadata: z2.record(z2.unknown()).optional()
3051
+ });
3052
+ function validateReport(value) {
3053
+ const parsed = reportSchema.safeParse(value);
3054
+ if (!parsed.success) {
3055
+ const issues = parsed.error.issues.map((issue) => ({
3056
+ path: issue.path.join(".") || "(root)",
3057
+ message: issue.message
3058
+ }));
3059
+ return {
3060
+ valid: false,
3061
+ issues,
3062
+ reason: `report failed schema validation (${issues.length} issue(s))`
3063
+ };
3064
+ }
3065
+ const report = parsed.data;
3066
+ report.findings = report.findings.map((finding) => ({
3067
+ ...finding,
3068
+ description: finding.description ?? finding.detail,
3069
+ ...finding.source === void 0 ? { source: { agent: report.agent, reportId: report.id, ...report.pipelineRunId !== void 0 ? { pipelineRunId: report.pipelineRunId } : {} } } : {}
3070
+ }));
3071
+ return { valid: true, report };
3072
+ }
3073
+ function normalizeEvidence(input) {
3074
+ if (input === void 0) return void 0;
3075
+ return input.map(
3076
+ (item) => typeof item === "string" ? { kind: "observation", excerpt: item } : item
3077
+ );
3078
+ }
3079
+
3080
+ // src/reports/aggregate.ts
2021
3081
  var SEVERITY_RANK = {
2022
3082
  info: 0,
2023
3083
  low: 1,
@@ -2039,7 +3099,12 @@ function aggregateReports(reports, options = {}) {
2039
3099
  }
2040
3100
  const sources = [...new Set(reports.map((r) => r.agent))].sort();
2041
3101
  const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
2042
- 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).`;
3102
+ const conflicts = detectConflicts(findingGroups);
3103
+ const agreements = findingGroups.filter(
3104
+ (group) => group.sources.length > 1 && group.severity !== void 0 && new Set(Object.values(group.severityBySource ?? {})).size === 1
3105
+ );
3106
+ const disputed = conflicts.length;
3107
+ 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).`;
2043
3108
  return {
2044
3109
  id: `combined-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
2045
3110
  title: options.objective !== void 0 ? truncate3(options.objective, 120) : "Combined report",
@@ -2048,10 +3113,57 @@ function aggregateReports(reports, options = {}) {
2048
3113
  taskIds: options.taskIds ?? [],
2049
3114
  summary,
2050
3115
  findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
3116
+ agreements,
3117
+ conflicts,
2051
3118
  recommendations: recommendationGroups,
2052
3119
  sources,
2053
3120
  reportIds: reports.map((r) => r.id),
2054
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
3121
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3122
+ metadata: {
3123
+ agents: sources.length,
3124
+ reports: reports.length,
3125
+ rawFindings: totalRawFindings,
3126
+ findingGroups: findingGroups.length,
3127
+ agreements: agreements.length,
3128
+ conflicts: conflicts.length
3129
+ }
3130
+ };
3131
+ }
3132
+ function rankConfidence(confidence) {
3133
+ return confidence ? { low: 1, medium: 2, high: 3 }[confidence] : 0;
3134
+ }
3135
+ function detectConflicts(groups) {
3136
+ const conflicts = [];
3137
+ for (const group of groups) {
3138
+ if (group.sources.length < 2) continue;
3139
+ const severities = new Set(Object.values(group.severityBySource ?? {}));
3140
+ if (severities.size > 1) {
3141
+ conflicts.push({
3142
+ type: "severity",
3143
+ findingTitle: group.title,
3144
+ positions: group.sources.map((source) => position(source, group)),
3145
+ 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.`
3146
+ });
3147
+ }
3148
+ const recommendations = (group.recommendations ?? []).filter((r) => r.length > 0);
3149
+ if (recommendations.length > 1) {
3150
+ conflicts.push({
3151
+ type: "recommendation",
3152
+ findingTitle: group.title,
3153
+ positions: group.sources.map((source) => position(source, group)),
3154
+ note: `Agents proposed ${recommendations.length} different fixes; none was discarded.`
3155
+ });
3156
+ }
3157
+ }
3158
+ return conflicts;
3159
+ }
3160
+ function position(source, group) {
3161
+ return {
3162
+ agent: source.agent,
3163
+ reportId: source.reportId,
3164
+ ...group.severityBySource?.[source.agent] !== void 0 ? { severity: group.severityBySource[source.agent] } : {},
3165
+ ...group.confidenceBySource?.[source.agent] !== void 0 ? { confidence: group.confidenceBySource[source.agent] } : {},
3166
+ ...group.recommendations?.[0] !== void 0 ? { recommendation: group.recommendations[0] } : {}
2055
3167
  };
2056
3168
  }
2057
3169
  function rank(finding) {
@@ -2064,10 +3176,22 @@ function mergeFinding(groups, finding, report, threshold) {
2064
3176
  group.occurrences += 1;
2065
3177
  group.details.push(finding.detail);
2066
3178
  group.sources.push(source);
2067
- group.evidence.push(...finding.evidence ?? []);
3179
+ group.evidence.push(...normalizeEvidence(finding.evidence) ?? []);
2068
3180
  if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
2069
3181
  group.severity = finding.severity;
2070
3182
  }
3183
+ if (finding.confidence && rankConfidence(finding.confidence) > rankConfidence(group.confidence)) {
3184
+ group.confidence = finding.confidence;
3185
+ }
3186
+ if (finding.severity) {
3187
+ group.severityBySource = { ...group.severityBySource, [report.agent]: finding.severity };
3188
+ }
3189
+ if (finding.confidence) {
3190
+ group.confidenceBySource = { ...group.confidenceBySource, [report.agent]: finding.confidence };
3191
+ }
3192
+ if (finding.recommendation && !group.recommendations?.includes(finding.recommendation)) {
3193
+ group.recommendations = [...group.recommendations ?? [], finding.recommendation];
3194
+ }
2071
3195
  return;
2072
3196
  }
2073
3197
  groups.push({
@@ -2076,7 +3200,11 @@ function mergeFinding(groups, finding, report, threshold) {
2076
3200
  sources: [source],
2077
3201
  occurrences: 1,
2078
3202
  ...finding.severity ? { severity: finding.severity } : {},
2079
- evidence: [...finding.evidence ?? []]
3203
+ ...finding.confidence ? { confidence: finding.confidence } : {},
3204
+ ...finding.severity ? { severityBySource: { [report.agent]: finding.severity } } : {},
3205
+ ...finding.confidence ? { confidenceBySource: { [report.agent]: finding.confidence } } : {},
3206
+ ...finding.recommendation ? { recommendations: [finding.recommendation] } : {},
3207
+ evidence: [...normalizeEvidence(finding.evidence) ?? []]
2080
3208
  });
2081
3209
  }
2082
3210
  function mergeRecommendation(groups, text, report, threshold) {
@@ -2158,6 +3286,7 @@ function extractReportFromText(raw, meta) {
2158
3286
  id: `${meta.taskId}-f-${index + 1}`,
2159
3287
  title: truncate4(text, 120),
2160
3288
  detail: text,
3289
+ description: text,
2161
3290
  severity: guessSeverity(text)
2162
3291
  }));
2163
3292
  return {
@@ -2168,7 +3297,8 @@ function extractReportFromText(raw, meta) {
2168
3297
  summary,
2169
3298
  findings,
2170
3299
  recommendations: recommendationTexts.map((t) => truncate4(t, 200)),
2171
- metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
3300
+ ...evidenceTexts.length > 0 ? { evidence: evidenceTexts.map((excerpt) => ({ kind: "observation", excerpt })) } : {},
3301
+ metadata: evidenceTexts.length > 0 ? { evidenceCount: evidenceTexts.length } : void 0,
2172
3302
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2173
3303
  };
2174
3304
  }
@@ -2184,27 +3314,46 @@ var ReportService = class {
2184
3314
  }
2185
3315
  store;
2186
3316
  log;
3317
+ /** Rejected reports from the last collection (§13 — malformed never enters aggregation). */
3318
+ lastDiagnostics = { collected: 0, accepted: 0, rejected: [] };
2187
3319
  get storeRef() {
2188
3320
  return this.store;
2189
3321
  }
2190
- /** Normalizes finished tasks into reports (§17 — normalizer). */
3322
+ /** Normalizes finished tasks into reports (§17 — normalizer), validating each
3323
+ * one so malformed reports never reach aggregation (§13). */
2191
3324
  async collectFromTasks(tasks) {
2192
- const reports = [];
3325
+ const candidates = [];
2193
3326
  for (const task of tasks) {
2194
3327
  const structured = task.result?.reports ?? [];
2195
3328
  if (structured.length > 0) {
2196
- reports.push(...structured);
3329
+ candidates.push(...structured);
2197
3330
  continue;
2198
3331
  }
2199
3332
  const text = task.result?.rawOutput ?? task.result?.summary ?? "";
2200
3333
  if (text.trim().length === 0) continue;
2201
- reports.push(
3334
+ candidates.push(
2202
3335
  extractReportFromText(text, {
2203
3336
  taskId: task.id,
2204
3337
  agent: task.agent ?? "unknown"
2205
3338
  })
2206
3339
  );
2207
3340
  }
3341
+ const reports = [];
3342
+ const rejected = [];
3343
+ for (const candidate of candidates) {
3344
+ const result = validateReport(candidate);
3345
+ if (result.valid) {
3346
+ reports.push(result.report);
3347
+ } else {
3348
+ rejected.push({
3349
+ key: candidate?.id ?? "(unknown)",
3350
+ reason: result.reason,
3351
+ issues: result.issues.map((issue) => `${issue.path}: ${issue.message}`)
3352
+ });
3353
+ this.log.warn(`Rejected malformed report: ${result.reason}`);
3354
+ }
3355
+ }
3356
+ this.lastDiagnostics = { collected: candidates.length, accepted: reports.length, rejected };
2208
3357
  return reports;
2209
3358
  }
2210
3359
  async saveReports(reports) {
@@ -2293,8 +3442,9 @@ function registerReportCommands(program) {
2293
3442
  return;
2294
3443
  }
2295
3444
  for (const c of combined) {
3445
+ const conflicts = c.conflicts.length > 0 ? ` ${c.conflicts.length} CONFLICT(S)` : "";
2296
3446
  console.log(
2297
- `${c.id} ${c.findings.length} finding(s) from ${c.sources.length} source(s)${c.pipelineRunId ? ` run=${c.pipelineRunId}` : ""}`
3447
+ `${c.id} ${c.findings.length} finding(s) from ${c.sources.length} source(s)${conflicts}${c.pipelineRunId ? ` run=${c.pipelineRunId}` : ""}`
2298
3448
  );
2299
3449
  }
2300
3450
  });
@@ -2331,6 +3481,21 @@ function registerReportCommands(program) {
2331
3481
  console.log(` reports: ${combined.reportIds.length}`);
2332
3482
  if (combined.pipelineRunId) console.log(` run: ${combined.pipelineRunId}`);
2333
3483
  console.log(` summary: ${combined.summary}`);
3484
+ if (combined.conflicts.length > 0) {
3485
+ console.log(` CONFLICTS (${combined.conflicts.length}) \u2014 both positions preserved:`);
3486
+ for (const conflict of combined.conflicts) {
3487
+ console.log(` ! ${conflict.findingTitle} [${conflict.type}]`);
3488
+ for (const position2 of conflict.positions) {
3489
+ console.log(
3490
+ ` ${position2.agent}: severity=${position2.severity ?? "unstated"}${position2.recommendation ? ` fix="${position2.recommendation}"` : ""}`
3491
+ );
3492
+ }
3493
+ console.log(` ${conflict.note}`);
3494
+ }
3495
+ }
3496
+ if (combined.agreements.length > 0) {
3497
+ console.log(` agreements: ${combined.agreements.length} (multi-agent consensus)`);
3498
+ }
2334
3499
  if (combined.findings.length > 0) {
2335
3500
  console.log(" findings (grouped, duplicates merged):");
2336
3501
  for (const finding of combined.findings) {
@@ -2424,7 +3589,7 @@ function truncate5(text, max) {
2424
3589
 
2425
3590
  // src/planner/model.ts
2426
3591
  import { randomBytes as randomBytes4 } from "crypto";
2427
- import { z as z2 } from "zod";
3592
+ import { z as z3 } from "zod";
2428
3593
 
2429
3594
  // src/planner/context.ts
2430
3595
  function buildPlannerContext(request) {
@@ -2477,21 +3642,21 @@ ${request.context}`);
2477
3642
  }
2478
3643
 
2479
3644
  // src/planner/model.ts
2480
- var planStepJsonSchema = z2.object({
2481
- id: z2.string().optional(),
2482
- title: z2.string().min(1),
2483
- description: z2.string().optional(),
2484
- order: z2.number().int().optional(),
2485
- targets: z2.array(z2.string()).optional(),
2486
- acceptance_criteria: z2.array(z2.string()).optional(),
2487
- depends_on: z2.array(z2.string()).optional(),
2488
- source_findings: z2.array(z2.string()).optional()
3645
+ var planStepJsonSchema = z3.object({
3646
+ id: z3.string().optional(),
3647
+ title: z3.string().min(1),
3648
+ description: z3.string().optional(),
3649
+ order: z3.number().int().optional(),
3650
+ targets: z3.array(z3.string()).optional(),
3651
+ acceptance_criteria: z3.array(z3.string()).optional(),
3652
+ depends_on: z3.array(z3.string()).optional(),
3653
+ source_findings: z3.array(z3.string()).optional()
2489
3654
  }).strict();
2490
- var planJsonSchema = z2.object({
2491
- summary: z2.string().min(1),
2492
- steps: z2.array(planStepJsonSchema).min(1),
2493
- risks: z2.array(z2.string()).optional(),
2494
- notes: z2.array(z2.string()).optional()
3655
+ var planJsonSchema = z3.object({
3656
+ summary: z3.string().min(1),
3657
+ steps: z3.array(planStepJsonSchema).min(1),
3658
+ risks: z3.array(z3.string()).optional(),
3659
+ notes: z3.array(z3.string()).optional()
2495
3660
  }).strict();
2496
3661
  var ModelPlanner = class {
2497
3662
  constructor(options) {
@@ -2584,9 +3749,23 @@ var PipelineEngine = class {
2584
3749
  }
2585
3750
  options;
2586
3751
  log;
3752
+ /** Runs executing in THIS process — cancellation can terminate them directly. */
3753
+ activeRuns = /* @__PURE__ */ new Map();
2587
3754
  get store() {
2588
3755
  return this.options.store;
2589
3756
  }
3757
+ /**
3758
+ * Saves the run while preserving fields that may be set by another process
3759
+ * (e.g. a cancellation request) — an in-memory snapshot must never clobber
3760
+ * externally requested cancellation (§9.2).
3761
+ */
3762
+ async persist(run2) {
3763
+ const current = await this.options.store.get(run2.id);
3764
+ await this.options.store.save({
3765
+ ...run2,
3766
+ ...current?.cancellationRequested === true ? { cancellationRequested: true, cancellationRequestedAt: current.cancellationRequestedAt } : {}
3767
+ });
3768
+ }
2590
3769
  /** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
2591
3770
  validate(def) {
2592
3771
  const ids = /* @__PURE__ */ new Set();
@@ -2686,18 +3865,44 @@ var PipelineEngine = class {
2686
3865
  );
2687
3866
  }
2688
3867
  }
3868
+ if (step.model) {
3869
+ const separator = step.model.indexOf(":");
3870
+ if (separator <= 0 || separator === step.model.length - 1) {
3871
+ throw new PipelineError(
3872
+ "PIPELINE_INVALID",
3873
+ `Step "${step.id}" has an invalid model reference "${step.model}" \u2014 use "provider:model-id".`
3874
+ );
3875
+ }
3876
+ const providerName = step.model.slice(0, separator);
3877
+ if (!this.options.providers.some((p) => p.name === providerName)) {
3878
+ throw new PipelineError(
3879
+ "PIPELINE_INVALID",
3880
+ `Step "${step.id}" uses model "${step.model}" but provider "${providerName}" is not configured.`
3881
+ );
3882
+ }
3883
+ }
2689
3884
  }
2690
3885
  const now = (/* @__PURE__ */ new Date()).toISOString();
2691
3886
  const run2 = {
2692
3887
  id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
2693
3888
  pipelineId: def.id,
3889
+ attempt: 1,
2694
3890
  objective,
2695
3891
  status: "running",
2696
3892
  startedAt: now,
2697
3893
  stepRuns: []
2698
3894
  };
2699
3895
  await this.options.store.save(run2);
2700
- this.log.info(`Running pipeline ${def.id} as ${run2.id}.`);
3896
+ const liveTaskIds = /* @__PURE__ */ new Set();
3897
+ this.activeRuns.set(run2.id, {
3898
+ taskIds: liveTaskIds,
3899
+ cancelTasks: async () => {
3900
+ for (const taskId of liveTaskIds) {
3901
+ await this.options.tasks.cancel(taskId).catch(() => void 0);
3902
+ }
3903
+ }
3904
+ });
3905
+ this.log.info(`Running pipeline ${def.id} as ${run2.id} (attempt ${run2.attempt}).`);
2701
3906
  await this.options.audit?.record({
2702
3907
  at: (/* @__PURE__ */ new Date()).toISOString(),
2703
3908
  action: "pipeline.run.started",
@@ -2709,7 +3914,22 @@ var PipelineEngine = class {
2709
3914
  const pending = new Set(def.steps.map((s) => s.id));
2710
3915
  const contextParts = [];
2711
3916
  const allReports = [];
3917
+ let cancelled = false;
2712
3918
  while (pending.size > 0) {
3919
+ const persisted = await this.options.store.get(run2.id);
3920
+ if (persisted?.cancellationRequested === true) {
3921
+ cancelled = true;
3922
+ run2.cancellationRequested = true;
3923
+ run2.cancellationRequestedAt = persisted.cancellationRequestedAt;
3924
+ await this.activeRuns.get(run2.id)?.cancelTasks();
3925
+ for (const step of def.steps.filter((s) => pending.has(s.id))) {
3926
+ const stepRun = { stepId: step.id, status: "cancelled" };
3927
+ run2.stepRuns.push(stepRun);
3928
+ options.onStep?.(stepRun);
3929
+ }
3930
+ pending.clear();
3931
+ break;
3932
+ }
2713
3933
  const ready = def.steps.filter(
2714
3934
  (step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
2715
3935
  );
@@ -2726,7 +3946,8 @@ var PipelineEngine = class {
2726
3946
  ...role ? { role } : {},
2727
3947
  combinedContext: contextParts.join("\n\n"),
2728
3948
  reports: [...allReports],
2729
- pipelineRunId: run2.id
3949
+ pipelineRunId: run2.id,
3950
+ runId: run2.id
2730
3951
  };
2731
3952
  return this.executeStep(step, ctx);
2732
3953
  })
@@ -2743,7 +3964,9 @@ ${contribution}`);
2743
3964
  }
2744
3965
  const stepRun = {
2745
3966
  stepId: step.id,
2746
- status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
3967
+ attempt: outcome.attempt ?? 1,
3968
+ ...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
3969
+ status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
2747
3970
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
2748
3971
  ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
2749
3972
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
@@ -2752,10 +3975,12 @@ ${contribution}`);
2752
3975
  options.onStep?.(stepRun);
2753
3976
  });
2754
3977
  run2.reports = [...allReports];
2755
- await this.options.store.save(run2);
3978
+ await this.persist(run2);
2756
3979
  }
2757
- const failed = [...outcomes.values()].some((o) => o.status === "failed");
2758
- const finalStatus = failed ? "failed" : "completed";
3980
+ const allOutcomes = [...outcomes.values()];
3981
+ const failed = !cancelled && allOutcomes.some((o) => o.status === "failed");
3982
+ const partial = !cancelled && !failed && allOutcomes.some((o) => o.status === "partial");
3983
+ const finalStatus = cancelled ? "cancelled" : failed ? "failed" : partial ? "partial" : "completed";
2759
3984
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
2760
3985
  let resultSummary;
2761
3986
  for (let i = def.steps.length - 1; i >= 0; i -= 1) {
@@ -2783,16 +4008,19 @@ ${contribution}`);
2783
4008
  run2.reports = collected;
2784
4009
  }
2785
4010
  }
4011
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
4012
+ this.activeRuns.delete(run2.id);
2786
4013
  const finished = {
2787
4014
  ...run2,
2788
4015
  status: finalStatus,
2789
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
4016
+ finishedAt,
4017
+ attempts: [...run2.attempts ?? [], { attempt: run2.attempt, startedAt: run2.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
2790
4018
  ...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
2791
4019
  ...combinedReportId !== void 0 ? { combinedReportId } : {},
2792
4020
  ...planId !== void 0 ? { planId } : {},
2793
4021
  ...resultSummary !== void 0 ? { resultSummary } : {}
2794
4022
  };
2795
- await this.options.store.save(finished);
4023
+ await this.persist(finished);
2796
4024
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
2797
4025
  await this.options.audit?.record({
2798
4026
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2802,6 +4030,23 @@ ${contribution}`);
2802
4030
  });
2803
4031
  return finished;
2804
4032
  }
4033
+ /** First-class cancellation (§9.2): requests cancellation and terminates
4034
+ * tasks that are running in this process immediately. */
4035
+ async cancel(runId) {
4036
+ const run2 = await this.options.store.get(runId);
4037
+ if (!run2) {
4038
+ throw new PipelineError("PIPELINE_NOT_FOUND", `Pipeline run "${runId}" does not exist.`);
4039
+ }
4040
+ await this.options.store.save({
4041
+ ...run2,
4042
+ cancellationRequested: true,
4043
+ cancellationRequestedAt: (/* @__PURE__ */ new Date()).toISOString()
4044
+ });
4045
+ const active = this.activeRuns.get(runId);
4046
+ if (active) {
4047
+ await active.cancelTasks();
4048
+ }
4049
+ }
2805
4050
  async get(runId) {
2806
4051
  return this.options.store.get(runId);
2807
4052
  }
@@ -2825,9 +4070,16 @@ ${contribution}`);
2825
4070
  this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
2826
4071
  }
2827
4072
  last = await this.executeStepOnce(step, ctx);
2828
- if (last.status !== "failed") return last;
4073
+ if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
2829
4074
  }
2830
- return last;
4075
+ return { ...last, attempt: retries + 1 };
4076
+ }
4077
+ /** Registers tasks created for a step as live, so cancellation can terminate them (§9.2). */
4078
+ trackLiveTasks(runId, taskIds) {
4079
+ if (!runId) return;
4080
+ const active = this.activeRuns.get(runId);
4081
+ if (!active) return;
4082
+ for (const id of taskIds) active.taskIds.add(id);
2831
4083
  }
2832
4084
  async executeStepOnce(step, ctx) {
2833
4085
  switch (step.kind) {
@@ -2860,7 +4112,10 @@ ${contribution}`);
2860
4112
  })
2861
4113
  )
2862
4114
  );
4115
+ this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
2863
4116
  const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
4117
+ const executionIds = results.map((r) => r.executions?.at(-1)?.executionId).filter((id) => id !== void 0);
4118
+ const completedTasks = results.filter((r) => r.status === "completed");
2864
4119
  const failedTasks = results.filter((r) => r.status !== "completed");
2865
4120
  const lines = results.map((result, index) => {
2866
4121
  const agentName = agentNames[index] ?? "unknown";
@@ -2870,16 +4125,25 @@ ${contribution}`);
2870
4125
  const reports = results.flatMap((r) => r.result?.reports ?? []);
2871
4126
  const summary = lines.join("\n");
2872
4127
  if (failedTasks.length > 0) {
2873
- const firstError = failedTasks[0]?.result?.error ?? "unknown error";
4128
+ const firstError = failedTasks[0]?.result?.error ?? failedTasks[0]?.lastError ?? "unknown outcome";
4129
+ const states = failedTasks.map((r) => r.status).join(", ");
2874
4130
  return {
2875
- status: "failed",
4131
+ // Some agents succeeded → the step produced usable but incomplete
4132
+ // intelligence: partial, not failed (roadmap §9.3).
4133
+ status: completedTasks.length > 0 ? "partial" : "failed",
2876
4134
  summary,
2877
4135
  taskIds: created.map((t) => t.id),
2878
- error: `${failedTasks.length}/${results.length} task(s) failed \u2014 ${firstError}`,
4136
+ error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
2879
4137
  reports
2880
4138
  };
2881
4139
  }
2882
- return { status: "completed", summary, taskIds: created.map((t) => t.id), reports };
4140
+ return {
4141
+ status: "completed",
4142
+ summary,
4143
+ taskIds: created.map((t) => t.id),
4144
+ ...executionIds.length > 0 ? { executionIds } : {},
4145
+ reports
4146
+ };
2883
4147
  }
2884
4148
  async runSingle(step, ctx, agentName) {
2885
4149
  const task = await this.options.tasks.create({
@@ -2888,10 +4152,13 @@ ${contribution}`);
2888
4152
  ...ctx.role ? { role: ctx.role.id } : {},
2889
4153
  ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
2890
4154
  });
4155
+ this.trackLiveTasks(ctx.runId, [task.id]);
2891
4156
  const result = await this.options.tasks.run(task.id);
2892
4157
  if (result.status !== "completed") {
2893
4158
  return {
2894
- status: "failed",
4159
+ // Unknown/timed_out outcomes are not failures — the work may have
4160
+ // happened remotely; the step is incomplete, not broken.
4161
+ status: result.status === "unknown" || result.status === "timed_out" ? "partial" : "failed",
2895
4162
  summary: result.result?.summary ?? "",
2896
4163
  taskIds: [task.id],
2897
4164
  error: result.result?.error ?? `task ${task.id} did not complete`,
@@ -3078,7 +4345,11 @@ function buildPipelineEngine(config, log = logger) {
3078
4345
  agents,
3079
4346
  roles,
3080
4347
  store: new FileTaskStore(),
3081
- ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
4348
+ ...config.project.memory !== void 0 ? {
4349
+ memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
4350
+ required: config.project.memory.required
4351
+ })
4352
+ } : {},
3082
4353
  audit: new FileAuditLog(),
3083
4354
  log
3084
4355
  });
@@ -3136,6 +4407,24 @@ function registerPipelineCommands(program) {
3136
4407
  }
3137
4408
  await runPipelineAction(def.id, objective ?? previous.objective);
3138
4409
  });
4410
+ pipeline.command("cancel <run-id>").description("Request cancellation of a pipeline run (stops scheduling and cancels running tasks).").action(async (runId) => {
4411
+ const config = loadConfig();
4412
+ const engine = buildPipelineEngine(config);
4413
+ await engine.cancel(runId);
4414
+ console.log(`Cancellation requested for run ${runId}.`);
4415
+ console.log("A run executing in another process stops at its next check point.");
4416
+ });
4417
+ pipeline.command("validate <id>").description("Validate a pipeline definition before running it (\xA79.5).").action(async (id) => {
4418
+ const config = loadConfig();
4419
+ const def = findPipeline(config, id);
4420
+ const engine = buildPipelineEngine(config);
4421
+ engine.validate(def);
4422
+ console.log(`Pipeline "${def.id}" is valid (${def.steps.length} step(s)).`);
4423
+ for (const step of def.steps) {
4424
+ const deps = step.dependsOn?.join(", ") ?? "(previous step)";
4425
+ console.log(` ${step.id} kind=${step.kind} depends_on=${deps}`);
4426
+ }
4427
+ });
3139
4428
  pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
3140
4429
  const config = loadConfig();
3141
4430
  const engine = buildPipelineEngine(config);
@@ -3145,7 +4434,8 @@ function registerPipelineCommands(program) {
3145
4434
  return;
3146
4435
  }
3147
4436
  for (const run2 of runs) {
3148
- console.log(`${run2.id} ${run2.status.padEnd(10)} ${run2.startedAt ?? "?"}`);
4437
+ const note = isInterrupted(run2) ? " (interrupted \u2014 process gone before finish)" : "";
4438
+ console.log(`${run2.id} ${run2.status.padEnd(10)} ${run2.startedAt ?? "?"}${note}`);
3149
4439
  }
3150
4440
  });
3151
4441
  pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
@@ -3163,6 +4453,13 @@ function registerPipelineCommands(program) {
3163
4453
  console.log(` status: ${run2.status}`);
3164
4454
  console.log(` started: ${run2.startedAt ?? "?"}`);
3165
4455
  if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
4456
+ console.log(` attempt: ${run2.attempt}`);
4457
+ if (run2.cancellationRequested) {
4458
+ console.log(` cancellation requested at ${run2.cancellationRequestedAt ?? "?"}`);
4459
+ }
4460
+ if (isInterrupted(run2)) {
4461
+ console.log(" note: this run looks interrupted (no finishedAt) \u2014 retry with `pipeline retry`");
4462
+ }
3166
4463
  if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
3167
4464
  for (const stepRun of run2.stepRuns) {
3168
4465
  console.log(` [${stepRun.stepId}] ${stepRun.status}`);
@@ -3204,6 +4501,9 @@ async function runPipelineAction(id, objective) {
3204
4501
  function registerRunAlias(program) {
3205
4502
  program.command("run <id> [objective]").description("Alias of `omninode pipeline run` \u2014 execute a configured pipeline end to end.").action(async (id, objective) => runPipelineAction(id, objective));
3206
4503
  }
4504
+ function isInterrupted(run2) {
4505
+ return run2.status === "running" && run2.finishedAt === void 0;
4506
+ }
3207
4507
  function findPipeline(config, id) {
3208
4508
  const def = config.project.pipelines.find((p) => p.id === id);
3209
4509
  if (!def) {