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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -166,7 +166,9 @@ var pipelineStepConfigSchema = z.object({
166
166
  model: z.string().optional(),
167
167
  depends_on: z.array(z.string()).optional(),
168
168
  retries: z.number().int().min(0).max(10).optional(),
169
- condition: z.enum(["always", "on-success", "on-failure"]).optional()
169
+ condition: z.enum(["always", "on-success", "on-failure"]).optional(),
170
+ /** Constraints passed to the planner for plan steps (§15). */
171
+ constraints: z.array(z.string()).optional()
170
172
  }).strict();
171
173
  var pipelineConfigSchema = z.object({
172
174
  id: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "pipeline id must be kebab-case"),
@@ -296,7 +298,8 @@ function toPipelineDefinition(raw) {
296
298
  model: step.model,
297
299
  dependsOn: step.depends_on,
298
300
  retries: step.retries,
299
- condition: step.condition
301
+ condition: step.condition,
302
+ constraints: step.constraints
300
303
  }))
301
304
  };
302
305
  }
@@ -1817,51 +1820,119 @@ function buildRoleRegistry(config) {
1817
1820
  }
1818
1821
 
1819
1822
  // src/persistence/json-file-store.ts
1820
- import { mkdir, readFile, rename, writeFile } from "fs/promises";
1823
+ import { open, mkdir, readFile, rename, stat } from "fs/promises";
1821
1824
  import { dirname } from "path";
1825
+ var SCHEMA_VERSION = 2;
1826
+ async function writeFileAtomic(target, contents) {
1827
+ const tmp = `${target}.tmp`;
1828
+ await mkdir(dirname(target), { recursive: true });
1829
+ const handle = await open(tmp, "w");
1830
+ try {
1831
+ await handle.writeFile(contents, "utf8");
1832
+ await handle.sync();
1833
+ } finally {
1834
+ await handle.close();
1835
+ }
1836
+ await rename(tmp, target);
1837
+ try {
1838
+ const dir = await open(dirname(target), "r");
1839
+ try {
1840
+ await dir.sync();
1841
+ } finally {
1842
+ await dir.close();
1843
+ }
1844
+ } catch {
1845
+ }
1846
+ }
1847
+ async function inspectDocument(path) {
1848
+ let raw;
1849
+ try {
1850
+ await stat(path);
1851
+ } catch {
1852
+ return { status: "missing" };
1853
+ }
1854
+ try {
1855
+ raw = await readFile(path, "utf8");
1856
+ } catch (error) {
1857
+ return { status: "corrupt", detail: `unreadable: ${error instanceof Error ? error.message : String(error)}` };
1858
+ }
1859
+ if (raw.trim().length === 0) return { status: "empty" };
1860
+ let parsed;
1861
+ try {
1862
+ parsed = JSON.parse(raw);
1863
+ } catch (error) {
1864
+ return { status: "corrupt", detail: error instanceof Error ? error.message : "invalid JSON" };
1865
+ }
1866
+ if (Array.isArray(parsed)) return { status: "v1-legacy" };
1867
+ if (parsed === null || typeof parsed !== "object") return { status: "corrupt", detail: "not an object" };
1868
+ const doc = parsed;
1869
+ if (!Array.isArray(doc.items)) return { status: "corrupt", detail: "missing items array" };
1870
+ const version = typeof doc.schemaVersion === "number" ? doc.schemaVersion : 1;
1871
+ if (version > SCHEMA_VERSION) return { status: "future", schemaVersion: version };
1872
+ if (version < SCHEMA_VERSION) return { status: "v1-legacy" };
1873
+ return { status: "current", schemaVersion: version, count: doc.items.length };
1874
+ }
1875
+ async function readDocument(path, onCorrupt, quarantine = true) {
1876
+ const state = await inspectDocument(path);
1877
+ if (state.status === "missing" || state.status === "empty") return [];
1878
+ if (state.status === "future") {
1879
+ const error = new Error(
1880
+ `${path} was written by a newer OmniNode (schema v${state.schemaVersion} > v${SCHEMA_VERSION})`
1881
+ );
1882
+ onCorrupt?.(path, error);
1883
+ return [];
1884
+ }
1885
+ let raw;
1886
+ try {
1887
+ raw = await readFile(path, "utf8");
1888
+ } catch (error) {
1889
+ onCorrupt?.(path, error);
1890
+ return [];
1891
+ }
1892
+ try {
1893
+ const parsed = JSON.parse(raw);
1894
+ if (Array.isArray(parsed)) return parsed;
1895
+ return parsed.items ?? [];
1896
+ } catch (error) {
1897
+ if (quarantine) {
1898
+ const quarantinePath = `${path}.corrupt-${Date.now()}`;
1899
+ try {
1900
+ await rename(path, quarantinePath);
1901
+ logger.child({ component: "persistence" }).warn(
1902
+ `Quarantined unreadable store file ${path} \u2192 ${quarantinePath}`
1903
+ );
1904
+ } catch {
1905
+ }
1906
+ }
1907
+ onCorrupt?.(path, error);
1908
+ return [];
1909
+ }
1910
+ }
1822
1911
  var JsonFileStore = class {
1823
1912
  filePath;
1824
1913
  schemaVersion;
1825
1914
  onCorrupt;
1915
+ quarantine;
1826
1916
  log;
1827
1917
  queue = Promise.resolve();
1828
1918
  constructor(options) {
1829
1919
  this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
1830
- this.schemaVersion = options.schemaVersion ?? 1;
1920
+ this.schemaVersion = options.schemaVersion ?? SCHEMA_VERSION;
1831
1921
  this.onCorrupt = options.onCorrupt;
1922
+ this.quarantine = options.quarantine ?? true;
1832
1923
  this.log = logger.child({ component: "persistence", file: options.fileName });
1833
1924
  }
1834
1925
  get path() {
1835
1926
  return this.filePath;
1836
1927
  }
1837
- /** All items, or [] when missing/corrupt. */
1928
+ /** All items, or [] when missing/migrated-from-v1/corrupt. */
1838
1929
  async readAll() {
1839
- let raw;
1840
- try {
1841
- raw = await readFile(this.filePath, "utf8");
1842
- } catch {
1843
- return [];
1844
- }
1845
- try {
1846
- const parsed = JSON.parse(raw);
1847
- if (Array.isArray(parsed)) return parsed;
1848
- if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.items)) {
1849
- return parsed.items;
1850
- }
1851
- throw new Error("persisted file has an unrecognized shape");
1852
- } catch (error) {
1853
- this.log.warn(`Treating ${this.filePath} as empty \u2014 it could not be parsed.`);
1854
- this.onCorrupt?.(this.filePath, error);
1855
- return [];
1856
- }
1930
+ return readDocument(this.filePath, this.onCorrupt, this.quarantine);
1857
1931
  }
1858
1932
  async writeAll(items) {
1859
1933
  const document = { schemaVersion: this.schemaVersion, items };
1860
- const tmp = `${this.filePath}.tmp`;
1861
- await mkdir(dirname(this.filePath), { recursive: true });
1862
- await writeFile(tmp, `${JSON.stringify(document, null, 2)}
1863
- `, "utf8");
1864
- await rename(tmp, this.filePath);
1934
+ await writeFileAtomic(this.filePath, `${JSON.stringify(document, null, 2)}
1935
+ `);
1865
1936
  }
1866
1937
  /** Serializes an async operation against this file. */
1867
1938
  synchronized(operation) {
@@ -2164,7 +2235,7 @@ function sleep(ms) {
2164
2235
  }
2165
2236
 
2166
2237
  // src/audit/index.ts
2167
- import { appendFile, mkdir as mkdir2 } from "fs/promises";
2238
+ import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
2168
2239
  import { dirname as dirname2 } from "path";
2169
2240
  var FileAuditLog = class {
2170
2241
  filePath;
@@ -2176,8 +2247,15 @@ var FileAuditLog = class {
2176
2247
  async record(event) {
2177
2248
  try {
2178
2249
  await mkdir2(dirname2(this.filePath), { recursive: true });
2179
- await appendFile(this.filePath, `${JSON.stringify(event)}
2250
+ const { open: open2 } = await import("fs/promises");
2251
+ const handle = await open2(this.filePath, "a");
2252
+ try {
2253
+ await handle.appendFile(`${JSON.stringify(event)}
2180
2254
  `, "utf8");
2255
+ await handle.sync();
2256
+ } finally {
2257
+ await handle.close();
2258
+ }
2181
2259
  } catch (error) {
2182
2260
  this.log.warn(
2183
2261
  `Audit write failed: ${error instanceof Error ? error.message : String(error)}`
@@ -2186,7 +2264,6 @@ var FileAuditLog = class {
2186
2264
  }
2187
2265
  /** Recent events, oldest first; used by the CLI. */
2188
2266
  async recent(limit = 20) {
2189
- const { readFile: readFile2 } = await import("fs/promises");
2190
2267
  let raw;
2191
2268
  try {
2192
2269
  raw = await readFile2(this.filePath, "utf8");
@@ -2580,10 +2657,98 @@ function createTaskEngine(config, log = logger, directory) {
2580
2657
  }
2581
2658
 
2582
2659
  // src/pipelines/engine.ts
2583
- import { randomBytes as randomBytes4 } from "crypto";
2660
+ import { randomBytes as randomBytes5 } from "crypto";
2584
2661
 
2585
- // src/planner/heuristic.ts
2662
+ // src/reports/aggregate.ts
2586
2663
  import { randomBytes as randomBytes2 } from "crypto";
2664
+
2665
+ // src/reports/schema.ts
2666
+ import { z as z2 } from "zod";
2667
+ var evidenceSchema = z2.union([
2668
+ z2.string().transform((excerpt) => ({ kind: "observation", excerpt })),
2669
+ z2.object({
2670
+ kind: z2.enum(["file", "line", "url", "command", "observation", "artifact"]),
2671
+ ref: z2.string().optional(),
2672
+ line: z2.number().int().optional(),
2673
+ excerpt: z2.string().optional(),
2674
+ agent: z2.string().optional()
2675
+ })
2676
+ ]);
2677
+ var findingSchema = z2.object({
2678
+ id: z2.string().min(1),
2679
+ title: z2.string().min(1),
2680
+ detail: z2.string().default(""),
2681
+ description: z2.string().optional(),
2682
+ severity: z2.enum(["info", "low", "medium", "high", "critical"]).optional(),
2683
+ confidence: z2.enum(["low", "medium", "high"]).optional(),
2684
+ severityBySource: z2.record(z2.enum(["info", "low", "medium", "high", "critical"])).optional(),
2685
+ confidenceBySource: z2.record(z2.enum(["low", "medium", "high"])).optional(),
2686
+ recommendations: z2.array(z2.string()).optional(),
2687
+ evidence: z2.array(evidenceSchema).optional(),
2688
+ location: z2.string().optional(),
2689
+ tags: z2.array(z2.string()).optional(),
2690
+ source: z2.object({ agent: z2.string().optional(), reportId: z2.string().optional(), pipelineRunId: z2.string().optional() }).optional(),
2691
+ recommendation: z2.string().optional()
2692
+ });
2693
+ var artifactSchema = z2.object({
2694
+ kind: z2.enum(["file", "patch", "log", "screenshot", "document", "data"]),
2695
+ ref: z2.string().min(1),
2696
+ description: z2.string().optional(),
2697
+ mediaType: z2.string().optional(),
2698
+ bytes: z2.number().int().optional()
2699
+ });
2700
+ var reportSchema = z2.object({
2701
+ id: z2.string().min(1),
2702
+ taskId: z2.string().min(1),
2703
+ agent: z2.string().min(1),
2704
+ agentId: z2.string().optional(),
2705
+ pipelineRunId: z2.string().optional(),
2706
+ model: z2.string().optional(),
2707
+ summary: z2.string(),
2708
+ findings: z2.array(findingSchema),
2709
+ recommendations: z2.array(z2.string()),
2710
+ confidence: z2.enum(["low", "medium", "high"]).optional(),
2711
+ evidence: z2.array(evidenceSchema).optional(),
2712
+ artifacts: z2.array(artifactSchema).optional(),
2713
+ createdAt: z2.string().min(1),
2714
+ metadata: z2.record(z2.unknown()).optional()
2715
+ });
2716
+ function validateReport(value) {
2717
+ const parsed = reportSchema.safeParse(value);
2718
+ if (!parsed.success) {
2719
+ const issues = parsed.error.issues.map((issue) => ({
2720
+ path: issue.path.join(".") || "(root)",
2721
+ message: issue.message
2722
+ }));
2723
+ return {
2724
+ valid: false,
2725
+ issues,
2726
+ reason: `report failed schema validation (${issues.length} issue(s))`
2727
+ };
2728
+ }
2729
+ const report = parsed.data;
2730
+ report.findings = report.findings.map((finding) => ({
2731
+ ...finding,
2732
+ description: finding.description ?? finding.detail,
2733
+ ...finding.source === void 0 ? { source: { agent: report.agent, reportId: report.id, ...report.pipelineRunId !== void 0 ? { pipelineRunId: report.pipelineRunId } : {} } } : {}
2734
+ }));
2735
+ return { valid: true, report };
2736
+ }
2737
+ function normalizeEvidence(input) {
2738
+ if (input === void 0) return void 0;
2739
+ return input.map(
2740
+ (item) => typeof item === "string" ? { kind: "observation", excerpt: item } : item
2741
+ );
2742
+ }
2743
+ function normalizeFinding(finding) {
2744
+ return {
2745
+ ...finding,
2746
+ description: finding.description ?? finding.detail,
2747
+ evidence: normalizeEvidence(finding.evidence)
2748
+ };
2749
+ }
2750
+
2751
+ // src/reports/aggregate.ts
2587
2752
  var SEVERITY_RANK = {
2588
2753
  info: 0,
2589
2754
  low: 1,
@@ -2591,23 +2756,197 @@ var SEVERITY_RANK = {
2591
2756
  high: 3,
2592
2757
  critical: 4
2593
2758
  };
2759
+ function aggregateReports(reports, options = {}) {
2760
+ const threshold = options.similarityThreshold ?? 0.7;
2761
+ const findingGroups = [];
2762
+ const recommendationGroups = [];
2763
+ for (const report of reports) {
2764
+ for (const finding of report.findings) {
2765
+ mergeFinding(findingGroups, finding, report, threshold);
2766
+ }
2767
+ for (const text of report.recommendations) {
2768
+ mergeRecommendation(recommendationGroups, text, report, threshold);
2769
+ }
2770
+ }
2771
+ const sources = [...new Set(reports.map((r) => r.agent))].sort();
2772
+ const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
2773
+ const conflicts = detectConflicts(findingGroups);
2774
+ const agreements = findingGroups.filter(
2775
+ (group) => group.sources.length > 1 && group.severity !== void 0 && new Set(Object.values(group.severityBySource ?? {})).size === 1
2776
+ );
2777
+ const disputed = conflicts.length;
2778
+ 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).`;
2779
+ return {
2780
+ id: `combined-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
2781
+ title: options.objective !== void 0 ? truncate(options.objective, 120) : "Combined report",
2782
+ ...options.objective !== void 0 ? { objective: options.objective } : {},
2783
+ ...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
2784
+ taskIds: options.taskIds ?? [],
2785
+ summary,
2786
+ findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
2787
+ agreements,
2788
+ conflicts,
2789
+ recommendations: recommendationGroups,
2790
+ sources,
2791
+ reportIds: reports.map((r) => r.id),
2792
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2793
+ metadata: {
2794
+ agents: sources.length,
2795
+ reports: reports.length,
2796
+ rawFindings: totalRawFindings,
2797
+ findingGroups: findingGroups.length,
2798
+ agreements: agreements.length,
2799
+ conflicts: conflicts.length
2800
+ }
2801
+ };
2802
+ }
2803
+ function rankConfidence(confidence) {
2804
+ return confidence ? { low: 1, medium: 2, high: 3 }[confidence] : 0;
2805
+ }
2806
+ function detectConflicts(groups) {
2807
+ const conflicts = [];
2808
+ for (const group of groups) {
2809
+ if (group.sources.length < 2) continue;
2810
+ const severities = new Set(Object.values(group.severityBySource ?? {}));
2811
+ if (severities.size > 1) {
2812
+ conflicts.push({
2813
+ type: "severity",
2814
+ findingTitle: group.title,
2815
+ positions: group.sources.map((source) => position(source, group)),
2816
+ 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.`
2817
+ });
2818
+ }
2819
+ const recommendations = (group.recommendations ?? []).filter((r) => r.length > 0);
2820
+ if (recommendations.length > 1) {
2821
+ conflicts.push({
2822
+ type: "recommendation",
2823
+ findingTitle: group.title,
2824
+ positions: group.sources.map((source) => position(source, group)),
2825
+ note: `Agents proposed ${recommendations.length} different fixes; none was discarded.`
2826
+ });
2827
+ }
2828
+ }
2829
+ return conflicts;
2830
+ }
2831
+ function position(source, group) {
2832
+ return {
2833
+ agent: source.agent,
2834
+ reportId: source.reportId,
2835
+ ...group.severityBySource?.[source.agent] !== void 0 ? { severity: group.severityBySource[source.agent] } : {},
2836
+ ...group.confidenceBySource?.[source.agent] !== void 0 ? { confidence: group.confidenceBySource[source.agent] } : {},
2837
+ ...group.recommendations?.[0] !== void 0 ? { recommendation: group.recommendations[0] } : {}
2838
+ };
2839
+ }
2840
+ function rank(finding) {
2841
+ return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
2842
+ }
2843
+ function mergeFinding(groups, finding, report, threshold) {
2844
+ const source = { agent: report.agent, reportId: report.id };
2845
+ const group = groups.find((g) => similar(g.title, finding.title, threshold));
2846
+ if (group) {
2847
+ group.occurrences += 1;
2848
+ group.details.push(finding.detail);
2849
+ group.sources.push(source);
2850
+ group.evidence.push(...normalizeEvidence(finding.evidence) ?? []);
2851
+ if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
2852
+ group.severity = finding.severity;
2853
+ }
2854
+ if (finding.confidence && rankConfidence(finding.confidence) > rankConfidence(group.confidence)) {
2855
+ group.confidence = finding.confidence;
2856
+ }
2857
+ if (finding.severity) {
2858
+ group.severityBySource = { ...group.severityBySource, [report.agent]: finding.severity };
2859
+ }
2860
+ if (finding.confidence) {
2861
+ group.confidenceBySource = { ...group.confidenceBySource, [report.agent]: finding.confidence };
2862
+ }
2863
+ if (finding.recommendation && !group.recommendations?.includes(finding.recommendation)) {
2864
+ group.recommendations = [...group.recommendations ?? [], finding.recommendation];
2865
+ }
2866
+ return;
2867
+ }
2868
+ groups.push({
2869
+ title: finding.title,
2870
+ details: [finding.detail],
2871
+ sources: [source],
2872
+ occurrences: 1,
2873
+ ...finding.severity ? { severity: finding.severity } : {},
2874
+ ...finding.confidence ? { confidence: finding.confidence } : {},
2875
+ ...finding.severity ? { severityBySource: { [report.agent]: finding.severity } } : {},
2876
+ ...finding.confidence ? { confidenceBySource: { [report.agent]: finding.confidence } } : {},
2877
+ ...finding.recommendation ? { recommendations: [finding.recommendation] } : {},
2878
+ evidence: [...normalizeEvidence(finding.evidence) ?? []]
2879
+ });
2880
+ }
2881
+ function mergeRecommendation(groups, text, report, threshold) {
2882
+ const source = { agent: report.agent, reportId: report.id };
2883
+ const group = groups.find((g) => similar(g.text, text, threshold));
2884
+ if (group) {
2885
+ group.occurrences += 1;
2886
+ group.sources.push(source);
2887
+ return;
2888
+ }
2889
+ groups.push({ text, sources: [source], occurrences: 1 });
2890
+ }
2891
+ function similar(a, b, threshold) {
2892
+ const left = tokens(a);
2893
+ const right = tokens(b);
2894
+ if (left.size === 0 || right.size === 0) return false;
2895
+ let intersection = 0;
2896
+ for (const token of left) {
2897
+ if (right.has(token)) intersection += 1;
2898
+ }
2899
+ const union = left.size + right.size - intersection;
2900
+ return intersection / union >= threshold;
2901
+ }
2902
+ function tokens(text) {
2903
+ return new Set(
2904
+ text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
2905
+ );
2906
+ }
2907
+ function truncate(text, max) {
2908
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
2909
+ }
2910
+
2911
+ // src/planner/heuristic.ts
2912
+ import { randomBytes as randomBytes3 } from "crypto";
2913
+ var SEVERITY_RANK2 = {
2914
+ info: 0,
2915
+ low: 1,
2916
+ medium: 2,
2917
+ high: 3,
2918
+ critical: 4
2919
+ };
2594
2920
  var HeuristicPlanner = class {
2595
2921
  name = "heuristic";
2596
2922
  async plan(request) {
2597
2923
  const steps = [];
2598
- const findings = request.reports.flatMap(
2599
- (report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
2600
- );
2601
2924
  const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
2602
- for (const [index, text] of recommendations.entries()) {
2925
+ for (const text of recommendations) {
2603
2926
  steps.push({
2604
2927
  id: `step-${steps.length + 1}`,
2605
- title: truncate(text, 120),
2928
+ title: truncate2(text, 120),
2606
2929
  order: steps.length + 1,
2607
- ...index > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
2930
+ ...steps.length > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
2608
2931
  });
2609
2932
  }
2610
- const significant = findings.filter(({ finding }) => finding.severity ? SEVERITY_RANK[finding.severity] >= SEVERITY_RANK.high : false).sort((a, b) => SEVERITY_RANK[b.finding.severity ?? "info"] - SEVERITY_RANK[a.finding.severity ?? "info"]);
2933
+ const contributed = request.aggregated ? request.aggregated.findings.map((finding) => ({
2934
+ finding: {
2935
+ id: `agg-${slug(finding.title)}`,
2936
+ title: finding.title,
2937
+ detail: finding.details[0] ?? finding.title,
2938
+ ...finding.severity ? { severity: finding.severity } : {},
2939
+ ...finding.recommendations && finding.recommendations.length > 0 ? { recommendation: finding.recommendations[0] } : {}
2940
+ },
2941
+ agent: finding.sources.map((s) => s.agent).join("+")
2942
+ })) : request.reports.flatMap(
2943
+ (report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
2944
+ );
2945
+ const significant = contributed.filter(
2946
+ ({ finding }) => finding.severity ? SEVERITY_RANK2[finding.severity] >= SEVERITY_RANK2.high : false
2947
+ ).sort(
2948
+ (a, b) => SEVERITY_RANK2[b.finding.severity ?? "info"] - SEVERITY_RANK2[a.finding.severity ?? "info"]
2949
+ );
2611
2950
  for (const { finding } of significant) {
2612
2951
  steps.push(remediationStep(finding, steps.length + 1));
2613
2952
  }
@@ -2620,12 +2959,20 @@ var HeuristicPlanner = class {
2620
2959
  });
2621
2960
  }
2622
2961
  const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
2962
+ const conflicts = request.aggregated?.conflicts ?? [];
2963
+ if (conflicts.length > 0) {
2964
+ risks.push(
2965
+ `${conflicts.length} unresolved agent disagreement(s): ${conflicts.map((c) => c.findingTitle).join("; ")}`
2966
+ );
2967
+ }
2623
2968
  return {
2624
- id: `plan-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
2969
+ id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
2625
2970
  objective: request.objective,
2971
+ goal: request.aggregated?.title ?? truncate2(request.objective, 120),
2626
2972
  summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
2627
2973
  steps,
2628
2974
  ...risks.length > 0 ? { risks } : {},
2975
+ ...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
2629
2976
  generatedBy: "heuristic",
2630
2977
  ...request.taskId !== void 0 ? { taskId: request.taskId } : {},
2631
2978
  ...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
@@ -2636,28 +2983,80 @@ var HeuristicPlanner = class {
2636
2983
  function remediationStep(finding, order) {
2637
2984
  return {
2638
2985
  id: `step-${order}`,
2639
- title: `Fix: ${truncate(finding.title, 100)}`,
2986
+ title: `Fix: ${truncate2(finding.title, 100)}`,
2640
2987
  description: finding.detail,
2641
2988
  order,
2642
2989
  dependsOn: order > 1 ? [`step-${order - 1}`] : [],
2643
- sourceFindings: [finding.id]
2990
+ sourceFindings: [finding.id],
2991
+ ...finding.recommendation ? { recommendation: finding.recommendation } : {}
2644
2992
  };
2645
2993
  }
2646
2994
  function dedupe(texts) {
2647
2995
  return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
2648
2996
  }
2649
- function truncate(text, max) {
2997
+ function slug(text) {
2998
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
2999
+ }
3000
+ function truncate2(text, max) {
2650
3001
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
2651
3002
  }
2652
3003
 
2653
- // src/planner/model.ts
2654
- import { randomBytes as randomBytes3 } from "crypto";
2655
- import { z as z2 } from "zod";
2656
-
3004
+ // src/planner/model.ts
3005
+ import { randomBytes as randomBytes4 } from "crypto";
3006
+
3007
+ // src/planner/schema.ts
3008
+ import { z as z3 } from "zod";
3009
+ var planStepSchema = z3.object({
3010
+ id: z3.string().min(1).optional(),
3011
+ title: z3.string().min(1),
3012
+ description: z3.string().optional(),
3013
+ order: z3.number().int().optional(),
3014
+ targets: z3.array(z3.string()).optional(),
3015
+ acceptance_criteria: z3.array(z3.string()).optional(),
3016
+ depends_on: z3.array(z3.string()).optional(),
3017
+ source_findings: z3.array(z3.string()).optional()
3018
+ });
3019
+ var planJsonSchema = z3.object({
3020
+ goal: z3.string().min(1),
3021
+ summary: z3.string().min(1),
3022
+ steps: z3.array(planStepSchema).min(1),
3023
+ risks: z3.array(z3.string()).optional(),
3024
+ notes: z3.array(z3.string()).optional()
3025
+ });
3026
+ function validatePlan(plan) {
3027
+ const issues = [];
3028
+ const stepIds = /* @__PURE__ */ new Set();
3029
+ for (const [index, step] of plan.steps.entries()) {
3030
+ if (stepIds.has(step.id)) {
3031
+ issues.push({ path: `steps.${index}.id`, message: `duplicate step id "${step.id}"` });
3032
+ }
3033
+ stepIds.add(step.id);
3034
+ if (!step.title || step.title.trim().length === 0) {
3035
+ issues.push({ path: `steps.${index}.title`, message: "title must not be empty" });
3036
+ }
3037
+ for (const dep of step.dependsOn ?? []) {
3038
+ if (!stepIds.has(dep) && !plan.steps.some((s) => s.id === dep)) {
3039
+ issues.push({ path: `steps.${index}.dependsOn`, message: `unknown step dependency "${dep}"` });
3040
+ }
3041
+ }
3042
+ }
3043
+ if (plan.steps.length === 0) {
3044
+ issues.push({ path: "steps", message: "a plan needs at least one step" });
3045
+ }
3046
+ if (!plan.summary || plan.summary.trim().length === 0) {
3047
+ issues.push({ path: "summary", message: "summary must not be empty" });
3048
+ }
3049
+ return issues.length > 0 ? { valid: false, issues, reason: `plan failed validation (${issues.length} issue(s))` } : { valid: true, plan };
3050
+ }
3051
+
2657
3052
  // src/planner/context.ts
2658
3053
  function buildPlannerContext(request) {
2659
- const parts = [`# Objective
3054
+ const parts = [`# Task
2660
3055
  ${request.objective}`];
3056
+ if (request.projectContext) {
3057
+ parts.push(`# Project context
3058
+ ${request.projectContext}`);
3059
+ }
2661
3060
  if (request.role) {
2662
3061
  const roleLines = [`# Role
2663
3062
  ${request.role.name} (${request.role.id})`];
@@ -2677,7 +3076,7 @@ ${request.role.name} (${request.role.id})`];
2677
3076
  )
2678
3077
  );
2679
3078
  parts.push(
2680
- `# Findings (${request.reports.length} report(s) by ${agents})
3079
+ `# Agent reports (${request.reports.length} by ${agents})
2681
3080
  ` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
2682
3081
  );
2683
3082
  const recommendationLines = request.reports.flatMap(
@@ -2687,9 +3086,20 @@ ${request.role.name} (${request.role.id})`];
2687
3086
  parts.push(`# Recommendations
2688
3087
  ${recommendationLines.join("\n")}`);
2689
3088
  }
2690
- const summaryLines = request.reports.map((report) => `- [${report.agent}] ${report.summary}`);
2691
- parts.push(`# Report summaries
2692
- ${summaryLines.join("\n")}`);
3089
+ }
3090
+ if (request.aggregated) {
3091
+ const aggregatedLines = [
3092
+ `- findings: ${request.aggregated.findings.length} (agreements: ${request.aggregated.agreements.length}, conflicts: ${request.aggregated.conflicts.length})`,
3093
+ ...request.aggregated.findings.map(
3094
+ (finding) => `- [${finding.severity ?? "info"}] ${finding.title} (x${finding.occurrences})`
3095
+ ),
3096
+ // Conflicts stay visible to the planner — it must plan around disagreement.
3097
+ ...request.aggregated.conflicts.map(
3098
+ (conflict) => `- CONFLICT (${conflict.type}) on "${conflict.findingTitle}": ${conflict.note}`
3099
+ )
3100
+ ];
3101
+ parts.push(`# Aggregated findings
3102
+ ${aggregatedLines.join("\n")}`);
2693
3103
  }
2694
3104
  if (request.memory && request.memory.length > 0) {
2695
3105
  parts.push(
@@ -2697,7 +3107,11 @@ ${summaryLines.join("\n")}`);
2697
3107
  ` + request.memory.map((entry) => `- [${entry.scope}] ${entry.content}`).join("\n")
2698
3108
  );
2699
3109
  }
2700
- if (request.context && request.context.length > 0) {
3110
+ if (request.constraints && request.constraints.length > 0) {
3111
+ parts.push(`# Constraints
3112
+ ${request.constraints.map((c) => `- ${c}`).join("\n")}`);
3113
+ }
3114
+ if (request.context) {
2701
3115
  parts.push(`# Research context
2702
3116
  ${request.context}`);
2703
3117
  }
@@ -2705,22 +3119,6 @@ ${request.context}`);
2705
3119
  }
2706
3120
 
2707
3121
  // src/planner/model.ts
2708
- var planStepJsonSchema = z2.object({
2709
- id: z2.string().optional(),
2710
- title: z2.string().min(1),
2711
- description: z2.string().optional(),
2712
- order: z2.number().int().optional(),
2713
- targets: z2.array(z2.string()).optional(),
2714
- acceptance_criteria: z2.array(z2.string()).optional(),
2715
- depends_on: z2.array(z2.string()).optional(),
2716
- source_findings: z2.array(z2.string()).optional()
2717
- }).strict();
2718
- var planJsonSchema = z2.object({
2719
- summary: z2.string().min(1),
2720
- steps: z2.array(planStepJsonSchema).min(1),
2721
- risks: z2.array(z2.string()).optional(),
2722
- notes: z2.array(z2.string()).optional()
2723
- }).strict();
2724
3122
  var ModelPlanner = class {
2725
3123
  constructor(options) {
2726
3124
  this.options = options;
@@ -2734,7 +3132,7 @@ var ModelPlanner = class {
2734
3132
  const messages = [
2735
3133
  {
2736
3134
  role: "system",
2737
- content: 'You are the OmniNode planner. Given the objective and the combined research context, produce a final analysis and a prioritized, actionable implementation plan. Respond with JSON ONLY (no prose, no code fences) matching exactly this shape: {"summary": string, "steps": [{"id": string, "title": string, "description": string, "order": number, "targets": string[], "acceptance_criteria": string[], "source_findings": string[]}], "risks": string[], "notes": string[]}. ' + (this.options.instruction ?? "")
3135
+ content: 'You are the OmniNode planner. Given the objective and the combined research context, produce a final analysis and a prioritized, actionable implementation plan. Respond with JSON ONLY (no prose, no code fences) matching exactly this shape: {"goal": string, "summary": string, "steps": [{"id": string, "title": string, "description": string, "order": number, "targets": string[], "acceptance_criteria": string[], "source_findings": string[]}], "risks": string[], "notes": string[]}. ' + (this.options.instruction ?? "")
2738
3136
  },
2739
3137
  { role: "user", content: context }
2740
3138
  ];
@@ -2745,60 +3143,86 @@ var ModelPlanner = class {
2745
3143
  return this.degrade(request, `planner model call failed: ${describe(error)}`);
2746
3144
  }
2747
3145
  const parsed = parsePlanJson(responseText);
2748
- if (!parsed) {
2749
- return this.degrade(request, "planner model output was not valid plan JSON");
3146
+ if (!parsed.ok) {
3147
+ return this.degrade(request, `planner model output rejected: ${parsed.reason}`, parsed.issues);
2750
3148
  }
2751
3149
  const plan = {
2752
- id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
3150
+ id: `plan-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
2753
3151
  objective: request.objective,
3152
+ goal: parsed.goal,
2754
3153
  summary: parsed.summary,
2755
3154
  steps: parsed.steps,
2756
3155
  ...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
2757
3156
  ...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
3157
+ ...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
2758
3158
  generatedBy: `model:${this.options.model}`,
2759
3159
  model: this.options.model,
2760
3160
  ...request.taskId !== void 0 ? { taskId: request.taskId } : {},
2761
3161
  ...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
2762
3162
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2763
3163
  };
2764
- return plan;
3164
+ const validation = validatePlan(plan);
3165
+ if (!validation.valid) {
3166
+ return this.degrade(request, `generated plan rejected: ${validation.reason}`, validation.issues);
3167
+ }
3168
+ return validation.plan;
2765
3169
  }
2766
- async degrade(request, reason) {
3170
+ async degrade(request, reason, issues = []) {
2767
3171
  if (this.options.fallback) {
2768
3172
  this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
2769
3173
  return this.options.fallback.plan(request);
2770
3174
  }
2771
- throw new PlannerError("PLANNER_FAILED", `Planner failed: ${reason}.`);
3175
+ throw new PlannerError("PLAN_INVALID", `Planner failed: ${reason}.`, {
3176
+ details: { reason, issues }
3177
+ });
2772
3178
  }
2773
3179
  };
2774
3180
  function parsePlanJson(text) {
2775
3181
  const unfenced = text.replace(/```(?:json)?/gi, "");
2776
3182
  const start = unfenced.indexOf("{");
2777
3183
  const end = unfenced.lastIndexOf("}");
2778
- if (start < 0 || end <= start) return void 0;
3184
+ if (start < 0 || end <= start) {
3185
+ return { ok: false, reason: "planner output contained no JSON object", issues: [] };
3186
+ }
3187
+ let json;
2779
3188
  try {
2780
- const json = JSON.parse(unfenced.slice(start, end + 1));
2781
- const parsed = planJsonSchema.safeParse(json);
2782
- if (!parsed.success) return void 0;
2783
- const steps = parsed.data.steps.map((step, index) => ({
2784
- id: step.id ?? `step-${index + 1}`,
2785
- title: step.title,
2786
- description: step.description,
2787
- order: step.order ?? index + 1,
2788
- targets: step.targets,
2789
- acceptanceCriteria: step.acceptance_criteria,
2790
- dependsOn: step.depends_on,
2791
- sourceFindings: step.source_findings
2792
- }));
3189
+ json = JSON.parse(unfenced.slice(start, end + 1));
3190
+ } catch (error) {
2793
3191
  return {
2794
- summary: parsed.data.summary,
2795
- steps: steps.sort((a, b) => a.order - b.order),
2796
- risks: parsed.data.risks,
2797
- notes: parsed.data.notes
3192
+ ok: false,
3193
+ reason: `planner output was not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
3194
+ issues: []
3195
+ };
3196
+ }
3197
+ const parsed = planJsonSchema.safeParse(json);
3198
+ if (!parsed.success) {
3199
+ return {
3200
+ ok: false,
3201
+ reason: "planner output did not match the plan schema",
3202
+ issues: parsed.error.issues.map((issue) => ({
3203
+ path: issue.path.join(".") || "(root)",
3204
+ message: issue.message
3205
+ }))
2798
3206
  };
2799
- } catch {
2800
- return void 0;
2801
3207
  }
3208
+ const steps = parsed.data.steps.map((step, index) => ({
3209
+ id: step.id ?? `step-${index + 1}`,
3210
+ title: step.title,
3211
+ description: step.description,
3212
+ order: step.order ?? index + 1,
3213
+ targets: step.targets,
3214
+ acceptanceCriteria: step.acceptance_criteria,
3215
+ dependsOn: step.depends_on,
3216
+ sourceFindings: step.source_findings
3217
+ })).sort((a, b) => a.order - b.order);
3218
+ return {
3219
+ ok: true,
3220
+ goal: parsed.data.goal,
3221
+ summary: parsed.data.summary,
3222
+ steps,
3223
+ risks: parsed.data.risks,
3224
+ notes: parsed.data.notes
3225
+ };
2802
3226
  }
2803
3227
  function describe(error) {
2804
3228
  return error instanceof Error ? error.message : String(error);
@@ -2947,7 +3371,7 @@ var PipelineEngine = class {
2947
3371
  }
2948
3372
  const now = (/* @__PURE__ */ new Date()).toISOString();
2949
3373
  const run = {
2950
- id: `run-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
3374
+ id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
2951
3375
  pipelineId: def.id,
2952
3376
  attempt: 1,
2953
3377
  objective,
@@ -3236,11 +3660,14 @@ ${contribution}`);
3236
3660
  };
3237
3661
  }
3238
3662
  async runPlan(step, ctx) {
3663
+ const aggregated = ctx.reports.length > 0 ? aggregateReports(ctx.reports) : void 0;
3239
3664
  const request = {
3240
3665
  objective: ctx.objective,
3241
3666
  reports: ctx.reports,
3667
+ ...aggregated !== void 0 ? { aggregated } : {},
3242
3668
  ...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
3243
3669
  ...ctx.role ? { role: ctx.role } : {},
3670
+ ...step.constraints !== void 0 && step.constraints.length > 0 ? { constraints: step.constraints } : {},
3244
3671
  ...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
3245
3672
  };
3246
3673
  let planner;
@@ -3399,255 +3826,6 @@ function buildPlanner(plannerConfig, chat, log = logger) {
3399
3826
  });
3400
3827
  }
3401
3828
 
3402
- // src/reports/aggregate.ts
3403
- import { randomBytes as randomBytes5 } from "crypto";
3404
-
3405
- // src/reports/schema.ts
3406
- import { z as z3 } from "zod";
3407
- var evidenceSchema = z3.union([
3408
- z3.string().transform((excerpt) => ({ kind: "observation", excerpt })),
3409
- z3.object({
3410
- kind: z3.enum(["file", "line", "url", "command", "observation", "artifact"]),
3411
- ref: z3.string().optional(),
3412
- line: z3.number().int().optional(),
3413
- excerpt: z3.string().optional(),
3414
- agent: z3.string().optional()
3415
- })
3416
- ]);
3417
- var findingSchema = z3.object({
3418
- id: z3.string().min(1),
3419
- title: z3.string().min(1),
3420
- detail: z3.string().default(""),
3421
- description: z3.string().optional(),
3422
- severity: z3.enum(["info", "low", "medium", "high", "critical"]).optional(),
3423
- confidence: z3.enum(["low", "medium", "high"]).optional(),
3424
- severityBySource: z3.record(z3.enum(["info", "low", "medium", "high", "critical"])).optional(),
3425
- confidenceBySource: z3.record(z3.enum(["low", "medium", "high"])).optional(),
3426
- recommendations: z3.array(z3.string()).optional(),
3427
- evidence: z3.array(evidenceSchema).optional(),
3428
- location: z3.string().optional(),
3429
- tags: z3.array(z3.string()).optional(),
3430
- source: z3.object({ agent: z3.string().optional(), reportId: z3.string().optional(), pipelineRunId: z3.string().optional() }).optional(),
3431
- recommendation: z3.string().optional()
3432
- });
3433
- var artifactSchema = z3.object({
3434
- kind: z3.enum(["file", "patch", "log", "screenshot", "document", "data"]),
3435
- ref: z3.string().min(1),
3436
- description: z3.string().optional(),
3437
- mediaType: z3.string().optional(),
3438
- bytes: z3.number().int().optional()
3439
- });
3440
- var reportSchema = z3.object({
3441
- id: z3.string().min(1),
3442
- taskId: z3.string().min(1),
3443
- agent: z3.string().min(1),
3444
- agentId: z3.string().optional(),
3445
- pipelineRunId: z3.string().optional(),
3446
- model: z3.string().optional(),
3447
- summary: z3.string(),
3448
- findings: z3.array(findingSchema),
3449
- recommendations: z3.array(z3.string()),
3450
- confidence: z3.enum(["low", "medium", "high"]).optional(),
3451
- evidence: z3.array(evidenceSchema).optional(),
3452
- artifacts: z3.array(artifactSchema).optional(),
3453
- createdAt: z3.string().min(1),
3454
- metadata: z3.record(z3.unknown()).optional()
3455
- });
3456
- function validateReport(value) {
3457
- const parsed = reportSchema.safeParse(value);
3458
- if (!parsed.success) {
3459
- const issues = parsed.error.issues.map((issue) => ({
3460
- path: issue.path.join(".") || "(root)",
3461
- message: issue.message
3462
- }));
3463
- return {
3464
- valid: false,
3465
- issues,
3466
- reason: `report failed schema validation (${issues.length} issue(s))`
3467
- };
3468
- }
3469
- const report = parsed.data;
3470
- report.findings = report.findings.map((finding) => ({
3471
- ...finding,
3472
- description: finding.description ?? finding.detail,
3473
- ...finding.source === void 0 ? { source: { agent: report.agent, reportId: report.id, ...report.pipelineRunId !== void 0 ? { pipelineRunId: report.pipelineRunId } : {} } } : {}
3474
- }));
3475
- return { valid: true, report };
3476
- }
3477
- function normalizeEvidence(input) {
3478
- if (input === void 0) return void 0;
3479
- return input.map(
3480
- (item) => typeof item === "string" ? { kind: "observation", excerpt: item } : item
3481
- );
3482
- }
3483
- function normalizeFinding(finding) {
3484
- return {
3485
- ...finding,
3486
- description: finding.description ?? finding.detail,
3487
- evidence: normalizeEvidence(finding.evidence)
3488
- };
3489
- }
3490
-
3491
- // src/reports/aggregate.ts
3492
- var SEVERITY_RANK2 = {
3493
- info: 0,
3494
- low: 1,
3495
- medium: 2,
3496
- high: 3,
3497
- critical: 4
3498
- };
3499
- function aggregateReports(reports, options = {}) {
3500
- const threshold = options.similarityThreshold ?? 0.7;
3501
- const findingGroups = [];
3502
- const recommendationGroups = [];
3503
- for (const report of reports) {
3504
- for (const finding of report.findings) {
3505
- mergeFinding(findingGroups, finding, report, threshold);
3506
- }
3507
- for (const text of report.recommendations) {
3508
- mergeRecommendation(recommendationGroups, text, report, threshold);
3509
- }
3510
- }
3511
- const sources = [...new Set(reports.map((r) => r.agent))].sort();
3512
- const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
3513
- const conflicts = detectConflicts(findingGroups);
3514
- const agreements = findingGroups.filter(
3515
- (group) => group.sources.length > 1 && group.severity !== void 0 && new Set(Object.values(group.severityBySource ?? {})).size === 1
3516
- );
3517
- const disputed = conflicts.length;
3518
- 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).`;
3519
- return {
3520
- id: `combined-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
3521
- title: options.objective !== void 0 ? truncate2(options.objective, 120) : "Combined report",
3522
- ...options.objective !== void 0 ? { objective: options.objective } : {},
3523
- ...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
3524
- taskIds: options.taskIds ?? [],
3525
- summary,
3526
- findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
3527
- agreements,
3528
- conflicts,
3529
- recommendations: recommendationGroups,
3530
- sources,
3531
- reportIds: reports.map((r) => r.id),
3532
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3533
- metadata: {
3534
- agents: sources.length,
3535
- reports: reports.length,
3536
- rawFindings: totalRawFindings,
3537
- findingGroups: findingGroups.length,
3538
- agreements: agreements.length,
3539
- conflicts: conflicts.length
3540
- }
3541
- };
3542
- }
3543
- function rankConfidence(confidence) {
3544
- return confidence ? { low: 1, medium: 2, high: 3 }[confidence] : 0;
3545
- }
3546
- function detectConflicts(groups) {
3547
- const conflicts = [];
3548
- for (const group of groups) {
3549
- if (group.sources.length < 2) continue;
3550
- const severities = new Set(Object.values(group.severityBySource ?? {}));
3551
- if (severities.size > 1) {
3552
- conflicts.push({
3553
- type: "severity",
3554
- findingTitle: group.title,
3555
- positions: group.sources.map((source) => position(source, group)),
3556
- note: `Agents disagree on severity (${[...severities].sort((a, b) => SEVERITY_RANK2[a] - SEVERITY_RANK2[b]).join(", ")}); the highest severity is shown, every position is preserved.`
3557
- });
3558
- }
3559
- const recommendations = (group.recommendations ?? []).filter((r) => r.length > 0);
3560
- if (recommendations.length > 1) {
3561
- conflicts.push({
3562
- type: "recommendation",
3563
- findingTitle: group.title,
3564
- positions: group.sources.map((source) => position(source, group)),
3565
- note: `Agents proposed ${recommendations.length} different fixes; none was discarded.`
3566
- });
3567
- }
3568
- }
3569
- return conflicts;
3570
- }
3571
- function position(source, group) {
3572
- return {
3573
- agent: source.agent,
3574
- reportId: source.reportId,
3575
- ...group.severityBySource?.[source.agent] !== void 0 ? { severity: group.severityBySource[source.agent] } : {},
3576
- ...group.confidenceBySource?.[source.agent] !== void 0 ? { confidence: group.confidenceBySource[source.agent] } : {},
3577
- ...group.recommendations?.[0] !== void 0 ? { recommendation: group.recommendations[0] } : {}
3578
- };
3579
- }
3580
- function rank(finding) {
3581
- return finding.severity ? SEVERITY_RANK2[finding.severity] : -1;
3582
- }
3583
- function mergeFinding(groups, finding, report, threshold) {
3584
- const source = { agent: report.agent, reportId: report.id };
3585
- const group = groups.find((g) => similar(g.title, finding.title, threshold));
3586
- if (group) {
3587
- group.occurrences += 1;
3588
- group.details.push(finding.detail);
3589
- group.sources.push(source);
3590
- group.evidence.push(...normalizeEvidence(finding.evidence) ?? []);
3591
- if (finding.severity && (!group.severity || SEVERITY_RANK2[finding.severity] > SEVERITY_RANK2[group.severity])) {
3592
- group.severity = finding.severity;
3593
- }
3594
- if (finding.confidence && rankConfidence(finding.confidence) > rankConfidence(group.confidence)) {
3595
- group.confidence = finding.confidence;
3596
- }
3597
- if (finding.severity) {
3598
- group.severityBySource = { ...group.severityBySource, [report.agent]: finding.severity };
3599
- }
3600
- if (finding.confidence) {
3601
- group.confidenceBySource = { ...group.confidenceBySource, [report.agent]: finding.confidence };
3602
- }
3603
- if (finding.recommendation && !group.recommendations?.includes(finding.recommendation)) {
3604
- group.recommendations = [...group.recommendations ?? [], finding.recommendation];
3605
- }
3606
- return;
3607
- }
3608
- groups.push({
3609
- title: finding.title,
3610
- details: [finding.detail],
3611
- sources: [source],
3612
- occurrences: 1,
3613
- ...finding.severity ? { severity: finding.severity } : {},
3614
- ...finding.confidence ? { confidence: finding.confidence } : {},
3615
- ...finding.severity ? { severityBySource: { [report.agent]: finding.severity } } : {},
3616
- ...finding.confidence ? { confidenceBySource: { [report.agent]: finding.confidence } } : {},
3617
- ...finding.recommendation ? { recommendations: [finding.recommendation] } : {},
3618
- evidence: [...normalizeEvidence(finding.evidence) ?? []]
3619
- });
3620
- }
3621
- function mergeRecommendation(groups, text, report, threshold) {
3622
- const source = { agent: report.agent, reportId: report.id };
3623
- const group = groups.find((g) => similar(g.text, text, threshold));
3624
- if (group) {
3625
- group.occurrences += 1;
3626
- group.sources.push(source);
3627
- return;
3628
- }
3629
- groups.push({ text, sources: [source], occurrences: 1 });
3630
- }
3631
- function similar(a, b, threshold) {
3632
- const left = tokens(a);
3633
- const right = tokens(b);
3634
- if (left.size === 0 || right.size === 0) return false;
3635
- let intersection = 0;
3636
- for (const token of left) {
3637
- if (right.has(token)) intersection += 1;
3638
- }
3639
- const union = left.size + right.size - intersection;
3640
- return intersection / union >= threshold;
3641
- }
3642
- function tokens(text) {
3643
- return new Set(
3644
- text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
3645
- );
3646
- }
3647
- function truncate2(text, max) {
3648
- return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
3649
- }
3650
-
3651
3829
  // src/reports/extract.ts
3652
3830
  var BULLET = /^(?:[-*•]|\d+[.)])\s+(.*)$/;
3653
3831
  function parseSections(raw) {
@@ -3872,9 +4050,59 @@ var ProjectStores = class {
3872
4050
  this.audit = new FileAuditLog(directory);
3873
4051
  }
3874
4052
  };
4053
+ var STORE_FILES = [
4054
+ "tasks.json",
4055
+ "pipelines.json",
4056
+ "reports.json",
4057
+ "plans.json",
4058
+ "memory.json"
4059
+ ];
4060
+ async function migrateProjectStores(directory = `${process.cwd()}/.omninode`, options = {}) {
4061
+ const reports = [];
4062
+ for (const file of STORE_FILES) {
4063
+ const path = `${directory}/${file}`;
4064
+ const state = await inspectDocument(path);
4065
+ if (state.status === "missing") {
4066
+ reports.push({ file, state: state.status, migrated: false });
4067
+ continue;
4068
+ }
4069
+ if (state.status === "current") {
4070
+ reports.push({ file, state: state.status, before: state.count, after: state.count, migrated: false });
4071
+ continue;
4072
+ }
4073
+ if (state.status === "future") {
4074
+ reports.push({
4075
+ file,
4076
+ state: state.status,
4077
+ migrated: false,
4078
+ note: `written by a newer OmniNode (schema v${state.schemaVersion}); not touched`
4079
+ });
4080
+ continue;
4081
+ }
4082
+ if (state.status === "corrupt") {
4083
+ reports.push({ file, state: state.status, migrated: false, note: state.detail });
4084
+ continue;
4085
+ }
4086
+ const items = await readDocument(path, void 0, false);
4087
+ if (!options.checkOnly) {
4088
+ await writeFileAtomic(path, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, items }, null, 2)}
4089
+ `);
4090
+ }
4091
+ const beforeCount = items.length;
4092
+ reports.push({
4093
+ file,
4094
+ state: state.status,
4095
+ before: beforeCount,
4096
+ after: items.length,
4097
+ migrated: !options.checkOnly,
4098
+ ...options.checkOnly ? { note: "would migrate" } : {}
4099
+ });
4100
+ }
4101
+ return reports;
4102
+ }
3875
4103
 
3876
4104
  // src/version.ts
3877
- var OMNINODE_VERSION = "2.0.0-alpha.10";
4105
+ var OMNINODE_VERSION = "2.0.0-alpha.12";
3878
4106
  export {
3879
4107
  AgentAdapterRegistry,
3880
4108
  AgentError,
@@ -3916,6 +4144,8 @@ export {
3916
4144
  RememberaMemoryProvider,
3917
4145
  ReportService,
3918
4146
  RoleRegistry,
4147
+ SCHEMA_VERSION,
4148
+ STORE_FILES,
3919
4149
  SUPPORTED_PROTOCOL_VERSIONS,
3920
4150
  TERMINAL_TASK_STATUSES,
3921
4151
  TaskEngine,
@@ -3951,6 +4181,7 @@ export {
3951
4181
  extractMessage,
3952
4182
  extractReportFromText,
3953
4183
  findConfigFile,
4184
+ inspectDocument,
3954
4185
  isNetworkError,
3955
4186
  isOmniNodeError,
3956
4187
  isRetryable,
@@ -3961,6 +4192,7 @@ export {
3961
4192
  kindFromStatus,
3962
4193
  loadConfig,
3963
4194
  logger,
4195
+ migrateProjectStores,
3964
4196
  newMessageId,
3965
4197
  normalizeEvidence,
3966
4198
  normalizeFinding,
@@ -3969,11 +4201,13 @@ export {
3969
4201
  parseRetryAfter,
3970
4202
  pipelineConfigSchema,
3971
4203
  pipelineStepConfigSchema,
4204
+ planJsonSchema,
3972
4205
  plannerConfigSchema,
3973
4206
  projectConfigSchema,
3974
4207
  providerConfigSchema,
3975
4208
  providerHttpError,
3976
4209
  providerTransportError,
4210
+ readDocument,
3977
4211
  registryKey,
3978
4212
  requestJson,
3979
4213
  requireCompletion,
@@ -3983,7 +4217,9 @@ export {
3983
4217
  sanitizeChunk,
3984
4218
  similar,
3985
4219
  validateEnvelope,
4220
+ validatePlan,
3986
4221
  validateReport,
3987
- validateWorkingDirectory
4222
+ validateWorkingDirectory,
4223
+ writeFileAtomic
3988
4224
  };
3989
4225
  //# sourceMappingURL=index.js.map