@hilbras/omninode 2.0.0-alpha.7 → 2.0.0-alpha.9

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/README.md CHANGED
@@ -18,9 +18,9 @@ existing intelligence; it does not try to become another model.
18
18
 
19
19
  ## Status
20
20
 
21
- **v2.0.0-alpha.7 — v2 Phase 7: OmniHilbras Integration v2**
21
+ **v2.0.0-alpha.9 — v2 Phase 9: Report System v2**
22
22
  (the reliability & interoperability line: architecture → execution →
23
- protocol → adapters → pipelines → providers → OmniHilbras) (the v2
23
+ protocol → adapters → pipelines → providers → OmniHilbras → memory → reports) (the v2
24
24
  roadmap is [docs/ROADMAP_V2.md](docs/ROADMAP_V2.md); the v1 plan is complete:
25
25
  the
26
26
  full loop from §28 runs in one command —
@@ -31,6 +31,19 @@ for the plan and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design).
31
31
 
32
32
  ### What's new in v2 (so far)
33
33
 
34
+ - **Phase 9 — Report System v2**: reports are validated machine-readable
35
+ objects — `validateReport()` runs on everything entering the system and
36
+ malformed reports are rejected with diagnostics instead of poisoning
37
+ aggregation. Findings gain description/confidence/source/recommendation,
38
+ evidence is typed (`file`, `line`, `url`, `command`, `observation`,
39
+ `artifact`), and reports carry artifacts and run provenance.
40
+ Details: [REPORTS.md](docs/REPORTS.md).
41
+ - **Phase 8 — Remembera / Memory v2**: the memory contract is formalized
42
+ (`retrieve` / `store` / `search` / `metadata` with v1 `query` / `write`
43
+ aliases kept working), memory gains structured **categories** (decisions,
44
+ problems, conventions, tasks…), retrieval is **task-oriented and budgeted**
45
+ (never "load all memory"), and `memory.required: true` makes backend
46
+ failures fatal instead of tolerated. Details: [MEMORY.md](docs/MEMORY.md).
34
47
  - **Phase 7 — OmniHilbras Integration v2**: the dedicated adapter now
35
48
  captures gateway-level metadata (version, tier, region) and attaches it
36
49
  to discovered models, honors provider-level `timeout_ms`, and supports
@@ -333,7 +346,7 @@ required for the core engine.
333
346
  - [Architecture](docs/ARCHITECTURE.md) — module map, workflow, local state
334
347
  - [Security model](docs/SECURITY.md) — secrets, process execution, env isolation, audit log
335
348
  - [API stability policy](docs/API_STABILITY.md) and [public API inventory](docs/API.md)
336
- - [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
349
+ - [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Memory](docs/MEMORY.md) · [Reports](docs/REPORTS.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
337
350
  - [Example project](examples/demo/) — a runnable end-to-end demo (`./demo.sh`)
338
351
 
339
352
  Prefer a container? `docker build -t omninode .` then
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.7";
9
+ var OMNINODE_VERSION = "2.0.0-alpha.9";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -134,7 +134,8 @@ var projectConfigSchema = z.object({
134
134
  provider: z.string().min(1).default("local"),
135
135
  base_url: z.string().url().optional(),
136
136
  api_key_env_var: z.string().min(1).optional(),
137
- timeout_ms: z.number().int().positive().optional()
137
+ /** When true, memory failures fail tasks instead of being tolerated (§12). */
138
+ required: z.boolean().optional()
138
139
  }).strict().optional()
139
140
  }).strict();
140
141
  var appConfigSchema = z.object({
@@ -216,7 +217,8 @@ function toAppConfig(yaml) {
216
217
  memory: {
217
218
  provider: yaml.project.memory.provider,
218
219
  baseUrl: yaml.project.memory.base_url,
219
- apiKeyEnvVar: yaml.project.memory.api_key_env_var
220
+ apiKeyEnvVar: yaml.project.memory.api_key_env_var,
221
+ required: yaml.project.memory.required
220
222
  }
221
223
  } : {}
222
224
  },
@@ -2490,6 +2492,23 @@ var FileAuditLog = class {
2490
2492
  }
2491
2493
  };
2492
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
+
2493
2512
  // src/memory/local.ts
2494
2513
  var STOPWORDS = /* @__PURE__ */ new Set([
2495
2514
  "the",
@@ -2552,9 +2571,27 @@ var LocalMemoryProvider = class extends JsonFileStore {
2552
2571
  constructor(directory) {
2553
2572
  super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
2554
2573
  }
2555
- 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) {
2556
2593
  const entries = await this.readAll();
2557
- 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(
2558
2595
  (entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
2559
2596
  );
2560
2597
  if (!query.text || query.text.trim().length === 0) {
@@ -2563,7 +2600,7 @@ var LocalMemoryProvider = class extends JsonFileStore {
2563
2600
  const queryKeywords = keywords(query.text);
2564
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);
2565
2602
  }
2566
- write(entry2) {
2603
+ store(entry2) {
2567
2604
  return this.mutate((entries) => {
2568
2605
  const index = entries.findIndex((e) => e.key === entry2.key);
2569
2606
  const stamped = {
@@ -2594,11 +2631,40 @@ var RememberaMemoryProvider = class {
2594
2631
  this.env = config.env ?? process.env;
2595
2632
  this.log = log.child({ component: "remembera" });
2596
2633
  }
2634
+ get authenticationConfigured() {
2635
+ return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
2636
+ }
2597
2637
  headers() {
2598
2638
  const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
2599
2639
  return key ? { authorization: `Bearer ${key}` } : {};
2600
2640
  }
2601
- 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) {
2602
2668
  const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
2603
2669
  if (status !== 200) {
2604
2670
  throw new MemoryError(`Remembera query returned HTTP ${status}.`);
@@ -2609,7 +2675,7 @@ var RememberaMemoryProvider = class {
2609
2675
  }
2610
2676
  return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
2611
2677
  }
2612
- async write(entry2) {
2678
+ async store(entry2) {
2613
2679
  const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
2614
2680
  if (status !== 200 && status !== 201) {
2615
2681
  throw new MemoryError(`Remembera write returned HTTP ${status}.`);
@@ -2644,6 +2710,7 @@ var RememberaMemoryProvider = class {
2644
2710
  content: entry2.content,
2645
2711
  scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
2646
2712
  ...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
2713
+ ...typeof entry2.category === "string" ? { category: entry2.category } : {},
2647
2714
  ...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
2648
2715
  ...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
2649
2716
  };
@@ -2674,57 +2741,54 @@ function createMemoryProvider(config) {
2674
2741
  }
2675
2742
 
2676
2743
  // src/memory/service.ts
2744
+ var DEFAULT_ENTRY_LIMIT = 8;
2677
2745
  var MAX_CONTEXT_CHARS = 4e3;
2678
2746
  var MemoryService = class {
2679
- constructor(provider, log = logger) {
2747
+ constructor(provider, log = logger, options = {}) {
2680
2748
  this.provider = provider;
2681
2749
  this.log = log.child({ component: "memory", provider: provider.name });
2750
+ this.required = options.required ?? false;
2751
+ this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
2682
2752
  }
2683
2753
  provider;
2684
2754
  log;
2755
+ required;
2756
+ maxContextChars;
2685
2757
  get providerRef() {
2686
2758
  return this.provider;
2687
2759
  }
2688
2760
  get providerName() {
2689
2761
  return this.provider.name;
2690
2762
  }
2691
- /** Relevant context for a task objective; empty string when nothing matches. */
2692
- 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;
2693
2769
  try {
2694
- const entries = await this.provider.query({
2770
+ entries = await this.provider.retrieve({
2695
2771
  text: query.objective,
2696
2772
  ...query.role ? { tags: [query.role] } : {},
2697
- limit: 8
2773
+ limit: query.limit ?? DEFAULT_ENTRY_LIMIT
2698
2774
  });
2699
- if (entries.length === 0) return "";
2700
- const byScope = /* @__PURE__ */ new Map();
2701
- for (const entry2 of entries) {
2702
- byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
2703
- }
2704
- const parts = [];
2705
- for (const scope of ["project", "task", "role", "knowledge"]) {
2706
- const scopeEntries = byScope.get(scope) ?? [];
2707
- if (scopeEntries.length === 0) continue;
2708
- const lines = scopeEntries.map((entry2) => `- ${entry2.content}`).join("\n");
2709
- parts.push(`### Relevant memory (${scope})
2710
- ${lines}`);
2711
- }
2712
- const context = parts.join("\n\n");
2713
- return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
2714
2775
  } catch (error) {
2715
- this.log.warn(
2716
- `Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
2717
- );
2718
- return "";
2776
+ return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
2719
2777
  }
2778
+ return this.format(entries);
2720
2779
  }
2721
- /** Records a finished task's outcome — best-effort. */
2780
+ /** Prompt-ready context text (v1 convenience wrapper). */
2781
+ async gatherContext(query) {
2782
+ return (await this.contextFor(query)).text;
2783
+ }
2784
+ /** Records a finished task's outcome — task history plus "known problems" (§12). */
2722
2785
  async recordTaskOutcome(task) {
2723
2786
  const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
2724
2787
  try {
2725
- await this.provider.write({
2788
+ await this.provider.store({
2726
2789
  key: `task:${task.id}`,
2727
2790
  scope: "task",
2791
+ category: "tasks",
2728
2792
  content: `Objective: ${task.objective}
2729
2793
  Status: ${task.status}
2730
2794
  Outcome: ${outcome}`,
@@ -2735,18 +2799,53 @@ Outcome: ${outcome}`,
2735
2799
  (report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
2736
2800
  );
2737
2801
  for (const { report, finding } of significant) {
2738
- await this.provider.write({
2802
+ await this.provider.store({
2739
2803
  key: `finding:${report.id}:${finding.id}`,
2740
2804
  scope: "project",
2805
+ category: "problems",
2741
2806
  content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
2742
2807
  tags: ["known-problem", report.agent]
2743
2808
  });
2744
2809
  }
2745
2810
  } catch (error) {
2746
- this.log.warn(
2747
- `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 }
2748
2845
  );
2749
2846
  }
2847
+ this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
2848
+ return fallback ?? { text: "", entries: [], truncated: false };
2750
2849
  }
2751
2850
  };
2752
2851
 
@@ -2780,7 +2879,11 @@ function createTaskEngine(config, log = logger, directory) {
2780
2879
  agents,
2781
2880
  roles,
2782
2881
  store: new FileTaskStore(directory),
2783
- ...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
+ } : {},
2784
2887
  audit: new FileAuditLog(),
2785
2888
  log
2786
2889
  });
@@ -2894,6 +2997,84 @@ function truncate2(text, max) {
2894
2997
 
2895
2998
  // src/reports/aggregate.ts
2896
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
+ evidence: z2.array(evidenceSchema).optional(),
3021
+ location: z2.string().optional(),
3022
+ tags: z2.array(z2.string()).optional(),
3023
+ source: z2.object({ agent: z2.string().optional(), reportId: z2.string().optional(), pipelineRunId: z2.string().optional() }).optional(),
3024
+ recommendation: z2.string().optional()
3025
+ });
3026
+ var artifactSchema = z2.object({
3027
+ kind: z2.enum(["file", "patch", "log", "screenshot", "document", "data"]),
3028
+ ref: z2.string().min(1),
3029
+ description: z2.string().optional(),
3030
+ mediaType: z2.string().optional(),
3031
+ bytes: z2.number().int().optional()
3032
+ });
3033
+ var reportSchema = z2.object({
3034
+ id: z2.string().min(1),
3035
+ taskId: z2.string().min(1),
3036
+ agent: z2.string().min(1),
3037
+ agentId: z2.string().optional(),
3038
+ pipelineRunId: z2.string().optional(),
3039
+ model: z2.string().optional(),
3040
+ summary: z2.string(),
3041
+ findings: z2.array(findingSchema),
3042
+ recommendations: z2.array(z2.string()),
3043
+ confidence: z2.enum(["low", "medium", "high"]).optional(),
3044
+ evidence: z2.array(evidenceSchema).optional(),
3045
+ artifacts: z2.array(artifactSchema).optional(),
3046
+ createdAt: z2.string().min(1),
3047
+ metadata: z2.record(z2.unknown()).optional()
3048
+ });
3049
+ function validateReport(value) {
3050
+ const parsed = reportSchema.safeParse(value);
3051
+ if (!parsed.success) {
3052
+ const issues = parsed.error.issues.map((issue) => ({
3053
+ path: issue.path.join(".") || "(root)",
3054
+ message: issue.message
3055
+ }));
3056
+ return {
3057
+ valid: false,
3058
+ issues,
3059
+ reason: `report failed schema validation (${issues.length} issue(s))`
3060
+ };
3061
+ }
3062
+ const report = parsed.data;
3063
+ report.findings = report.findings.map((finding) => ({
3064
+ ...finding,
3065
+ description: finding.description ?? finding.detail,
3066
+ ...finding.source === void 0 ? { source: { agent: report.agent, reportId: report.id, ...report.pipelineRunId !== void 0 ? { pipelineRunId: report.pipelineRunId } : {} } } : {}
3067
+ }));
3068
+ return { valid: true, report };
3069
+ }
3070
+ function normalizeEvidence(input) {
3071
+ if (input === void 0) return void 0;
3072
+ return input.map(
3073
+ (item) => typeof item === "string" ? { kind: "observation", excerpt: item } : item
3074
+ );
3075
+ }
3076
+
3077
+ // src/reports/aggregate.ts
2897
3078
  var SEVERITY_RANK = {
2898
3079
  info: 0,
2899
3080
  low: 1,
@@ -2940,7 +3121,7 @@ function mergeFinding(groups, finding, report, threshold) {
2940
3121
  group.occurrences += 1;
2941
3122
  group.details.push(finding.detail);
2942
3123
  group.sources.push(source);
2943
- group.evidence.push(...finding.evidence ?? []);
3124
+ group.evidence.push(...normalizeEvidence(finding.evidence) ?? []);
2944
3125
  if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
2945
3126
  group.severity = finding.severity;
2946
3127
  }
@@ -2952,7 +3133,7 @@ function mergeFinding(groups, finding, report, threshold) {
2952
3133
  sources: [source],
2953
3134
  occurrences: 1,
2954
3135
  ...finding.severity ? { severity: finding.severity } : {},
2955
- evidence: [...finding.evidence ?? []]
3136
+ evidence: [...normalizeEvidence(finding.evidence) ?? []]
2956
3137
  });
2957
3138
  }
2958
3139
  function mergeRecommendation(groups, text, report, threshold) {
@@ -3034,6 +3215,7 @@ function extractReportFromText(raw, meta) {
3034
3215
  id: `${meta.taskId}-f-${index + 1}`,
3035
3216
  title: truncate4(text, 120),
3036
3217
  detail: text,
3218
+ description: text,
3037
3219
  severity: guessSeverity(text)
3038
3220
  }));
3039
3221
  return {
@@ -3044,7 +3226,8 @@ function extractReportFromText(raw, meta) {
3044
3226
  summary,
3045
3227
  findings,
3046
3228
  recommendations: recommendationTexts.map((t) => truncate4(t, 200)),
3047
- metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
3229
+ ...evidenceTexts.length > 0 ? { evidence: evidenceTexts.map((excerpt) => ({ kind: "observation", excerpt })) } : {},
3230
+ metadata: evidenceTexts.length > 0 ? { evidenceCount: evidenceTexts.length } : void 0,
3048
3231
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3049
3232
  };
3050
3233
  }
@@ -3060,27 +3243,46 @@ var ReportService = class {
3060
3243
  }
3061
3244
  store;
3062
3245
  log;
3246
+ /** Rejected reports from the last collection (§13 — malformed never enters aggregation). */
3247
+ lastDiagnostics = { collected: 0, accepted: 0, rejected: [] };
3063
3248
  get storeRef() {
3064
3249
  return this.store;
3065
3250
  }
3066
- /** Normalizes finished tasks into reports (§17 — normalizer). */
3251
+ /** Normalizes finished tasks into reports (§17 — normalizer), validating each
3252
+ * one so malformed reports never reach aggregation (§13). */
3067
3253
  async collectFromTasks(tasks) {
3068
- const reports = [];
3254
+ const candidates = [];
3069
3255
  for (const task of tasks) {
3070
3256
  const structured = task.result?.reports ?? [];
3071
3257
  if (structured.length > 0) {
3072
- reports.push(...structured);
3258
+ candidates.push(...structured);
3073
3259
  continue;
3074
3260
  }
3075
3261
  const text = task.result?.rawOutput ?? task.result?.summary ?? "";
3076
3262
  if (text.trim().length === 0) continue;
3077
- reports.push(
3263
+ candidates.push(
3078
3264
  extractReportFromText(text, {
3079
3265
  taskId: task.id,
3080
3266
  agent: task.agent ?? "unknown"
3081
3267
  })
3082
3268
  );
3083
3269
  }
3270
+ const reports = [];
3271
+ const rejected = [];
3272
+ for (const candidate of candidates) {
3273
+ const result = validateReport(candidate);
3274
+ if (result.valid) {
3275
+ reports.push(result.report);
3276
+ } else {
3277
+ rejected.push({
3278
+ key: candidate?.id ?? "(unknown)",
3279
+ reason: result.reason,
3280
+ issues: result.issues.map((issue) => `${issue.path}: ${issue.message}`)
3281
+ });
3282
+ this.log.warn(`Rejected malformed report: ${result.reason}`);
3283
+ }
3284
+ }
3285
+ this.lastDiagnostics = { collected: candidates.length, accepted: reports.length, rejected };
3084
3286
  return reports;
3085
3287
  }
3086
3288
  async saveReports(reports) {
@@ -3300,7 +3502,7 @@ function truncate5(text, max) {
3300
3502
 
3301
3503
  // src/planner/model.ts
3302
3504
  import { randomBytes as randomBytes4 } from "crypto";
3303
- import { z as z2 } from "zod";
3505
+ import { z as z3 } from "zod";
3304
3506
 
3305
3507
  // src/planner/context.ts
3306
3508
  function buildPlannerContext(request) {
@@ -3353,21 +3555,21 @@ ${request.context}`);
3353
3555
  }
3354
3556
 
3355
3557
  // src/planner/model.ts
3356
- var planStepJsonSchema = z2.object({
3357
- id: z2.string().optional(),
3358
- title: z2.string().min(1),
3359
- description: z2.string().optional(),
3360
- order: z2.number().int().optional(),
3361
- targets: z2.array(z2.string()).optional(),
3362
- acceptance_criteria: z2.array(z2.string()).optional(),
3363
- depends_on: z2.array(z2.string()).optional(),
3364
- source_findings: z2.array(z2.string()).optional()
3558
+ var planStepJsonSchema = z3.object({
3559
+ id: z3.string().optional(),
3560
+ title: z3.string().min(1),
3561
+ description: z3.string().optional(),
3562
+ order: z3.number().int().optional(),
3563
+ targets: z3.array(z3.string()).optional(),
3564
+ acceptance_criteria: z3.array(z3.string()).optional(),
3565
+ depends_on: z3.array(z3.string()).optional(),
3566
+ source_findings: z3.array(z3.string()).optional()
3365
3567
  }).strict();
3366
- var planJsonSchema = z2.object({
3367
- summary: z2.string().min(1),
3368
- steps: z2.array(planStepJsonSchema).min(1),
3369
- risks: z2.array(z2.string()).optional(),
3370
- notes: z2.array(z2.string()).optional()
3568
+ var planJsonSchema = z3.object({
3569
+ summary: z3.string().min(1),
3570
+ steps: z3.array(planStepJsonSchema).min(1),
3571
+ risks: z3.array(z3.string()).optional(),
3572
+ notes: z3.array(z3.string()).optional()
3371
3573
  }).strict();
3372
3574
  var ModelPlanner = class {
3373
3575
  constructor(options) {
@@ -4056,7 +4258,11 @@ function buildPipelineEngine(config, log = logger) {
4056
4258
  agents,
4057
4259
  roles,
4058
4260
  store: new FileTaskStore(),
4059
- ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
4261
+ ...config.project.memory !== void 0 ? {
4262
+ memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
4263
+ required: config.project.memory.required
4264
+ })
4265
+ } : {},
4060
4266
  audit: new FileAuditLog(),
4061
4267
  log
4062
4268
  });