@hilbras/omninode 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -39,6 +39,11 @@ var ProtocolError = class extends OmniNodeError {
39
39
  super("PROTOCOL_VIOLATION", message, options);
40
40
  }
41
41
  };
42
+ var PlannerError = class extends OmniNodeError {
43
+ constructor(code, message, options = {}) {
44
+ super(code, message, options);
45
+ }
46
+ };
42
47
  var MemoryError = class extends OmniNodeError {
43
48
  constructor(message, options = {}) {
44
49
  super("MEMORY_UNAVAILABLE", message, options);
@@ -154,12 +159,18 @@ var pipelineConfigSchema = z.object({
154
159
  role: z.string().optional(),
155
160
  steps: z.array(pipelineStepConfigSchema).min(1)
156
161
  }).strict();
162
+ var plannerConfigSchema = z.object({
163
+ kind: z.enum(["model", "heuristic"]).default("model"),
164
+ model: z.string().optional(),
165
+ instruction: z.string().optional()
166
+ }).strict();
157
167
  var projectConfigSchema = z.object({
158
168
  name: z.string().min(1),
159
169
  providers: z.array(providerConfigSchema).default([]),
160
170
  agents: z.array(agentConfigSchema).default([]),
161
171
  roles: z.array(roleConfigSchema).default([]),
162
172
  pipelines: z.array(pipelineConfigSchema).default([]),
173
+ planner: plannerConfigSchema.optional(),
163
174
  memory: z.object({
164
175
  provider: z.string().min(1).default("local"),
165
176
  base_url: z.string().url().optional(),
@@ -234,6 +245,13 @@ function toAppConfig(yaml) {
234
245
  agents: yaml.project.agents.map(toAgentConfig),
235
246
  roles: yaml.project.roles.map(toRoleDefinition),
236
247
  pipelines: yaml.project.pipelines.map(toPipelineDefinition),
248
+ ...yaml.project.planner !== void 0 ? {
249
+ planner: {
250
+ kind: yaml.project.planner.kind,
251
+ model: yaml.project.planner.model,
252
+ instruction: yaml.project.planner.instruction
253
+ }
254
+ } : {},
237
255
  ...yaml.project.memory !== void 0 ? {
238
256
  memory: {
239
257
  provider: yaml.project.memory.provider,
@@ -1479,7 +1497,231 @@ function createTaskEngine(config, log = logger, directory) {
1479
1497
  }
1480
1498
 
1481
1499
  // src/pipelines/engine.ts
1500
+ import { randomBytes as randomBytes4 } from "crypto";
1501
+
1502
+ // src/planner/heuristic.ts
1482
1503
  import { randomBytes as randomBytes2 } from "crypto";
1504
+ var SEVERITY_RANK = {
1505
+ info: 0,
1506
+ low: 1,
1507
+ medium: 2,
1508
+ high: 3,
1509
+ critical: 4
1510
+ };
1511
+ var HeuristicPlanner = class {
1512
+ name = "heuristic";
1513
+ async plan(request) {
1514
+ const steps = [];
1515
+ const findings = request.reports.flatMap(
1516
+ (report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
1517
+ );
1518
+ const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
1519
+ for (const [index, text] of recommendations.entries()) {
1520
+ steps.push({
1521
+ id: `step-${steps.length + 1}`,
1522
+ title: truncate(text, 120),
1523
+ order: steps.length + 1,
1524
+ ...index > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
1525
+ });
1526
+ }
1527
+ 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"]);
1528
+ for (const { finding } of significant) {
1529
+ steps.push(remediationStep(finding, steps.length + 1));
1530
+ }
1531
+ if (steps.length === 0) {
1532
+ steps.push({
1533
+ id: "step-1",
1534
+ title: "Gather additional information",
1535
+ description: "The research reports contained no actionable recommendations or significant findings. Investigate the objective further before planning implementation work.",
1536
+ order: 1
1537
+ });
1538
+ }
1539
+ const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
1540
+ return {
1541
+ id: `plan-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
1542
+ objective: request.objective,
1543
+ summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
1544
+ steps,
1545
+ ...risks.length > 0 ? { risks } : {},
1546
+ generatedBy: "heuristic",
1547
+ ...request.taskId !== void 0 ? { taskId: request.taskId } : {},
1548
+ ...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
1549
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1550
+ };
1551
+ }
1552
+ };
1553
+ function remediationStep(finding, order) {
1554
+ return {
1555
+ id: `step-${order}`,
1556
+ title: `Fix: ${truncate(finding.title, 100)}`,
1557
+ description: finding.detail,
1558
+ order,
1559
+ dependsOn: order > 1 ? [`step-${order - 1}`] : [],
1560
+ sourceFindings: [finding.id]
1561
+ };
1562
+ }
1563
+ function dedupe(texts) {
1564
+ return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
1565
+ }
1566
+ function truncate(text, max) {
1567
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1568
+ }
1569
+
1570
+ // src/planner/model.ts
1571
+ import { randomBytes as randomBytes3 } from "crypto";
1572
+ import { z as z2 } from "zod";
1573
+
1574
+ // src/planner/context.ts
1575
+ function buildPlannerContext(request) {
1576
+ const parts = [`# Objective
1577
+ ${request.objective}`];
1578
+ if (request.role) {
1579
+ const roleLines = [`# Role
1580
+ ${request.role.name} (${request.role.id})`];
1581
+ if (request.role.responsibilities.length > 0) {
1582
+ roleLines.push("Responsibilities:", ...request.role.responsibilities.map((r) => `- ${r}`));
1583
+ }
1584
+ if (request.role.expectedOutputs.length > 0) {
1585
+ roleLines.push("Expected outputs:", ...request.role.expectedOutputs.map((o) => `- ${o}`));
1586
+ }
1587
+ parts.push(roleLines.join("\n"));
1588
+ }
1589
+ if (request.reports.length > 0) {
1590
+ const agents = [...new Set(request.reports.map((r) => r.agent))].join(", ");
1591
+ const findingLines = request.reports.flatMap(
1592
+ (report) => report.findings.map(
1593
+ (finding) => `- [${finding.severity ?? "info"}] ${finding.title} (source: ${report.agent}, finding: ${finding.id})` + (finding.detail ? ` \u2014 ${finding.detail}` : "")
1594
+ )
1595
+ );
1596
+ parts.push(
1597
+ `# Findings (${request.reports.length} report(s) by ${agents})
1598
+ ` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
1599
+ );
1600
+ const recommendationLines = request.reports.flatMap(
1601
+ (report) => report.recommendations.map((text) => `- ${text} (source: ${report.agent})`)
1602
+ );
1603
+ if (recommendationLines.length > 0) {
1604
+ parts.push(`# Recommendations
1605
+ ${recommendationLines.join("\n")}`);
1606
+ }
1607
+ const summaryLines = request.reports.map((report) => `- [${report.agent}] ${report.summary}`);
1608
+ parts.push(`# Report summaries
1609
+ ${summaryLines.join("\n")}`);
1610
+ }
1611
+ if (request.memory && request.memory.length > 0) {
1612
+ parts.push(
1613
+ `# Relevant memory
1614
+ ` + request.memory.map((entry) => `- [${entry.scope}] ${entry.content}`).join("\n")
1615
+ );
1616
+ }
1617
+ if (request.context && request.context.length > 0) {
1618
+ parts.push(`# Research context
1619
+ ${request.context}`);
1620
+ }
1621
+ return parts.join("\n\n");
1622
+ }
1623
+
1624
+ // src/planner/model.ts
1625
+ var planStepJsonSchema = z2.object({
1626
+ id: z2.string().optional(),
1627
+ title: z2.string().min(1),
1628
+ description: z2.string().optional(),
1629
+ order: z2.number().int().optional(),
1630
+ targets: z2.array(z2.string()).optional(),
1631
+ acceptance_criteria: z2.array(z2.string()).optional(),
1632
+ depends_on: z2.array(z2.string()).optional(),
1633
+ source_findings: z2.array(z2.string()).optional()
1634
+ }).strict();
1635
+ var planJsonSchema = z2.object({
1636
+ summary: z2.string().min(1),
1637
+ steps: z2.array(planStepJsonSchema).min(1),
1638
+ risks: z2.array(z2.string()).optional(),
1639
+ notes: z2.array(z2.string()).optional()
1640
+ }).strict();
1641
+ var ModelPlanner = class {
1642
+ constructor(options) {
1643
+ this.options = options;
1644
+ this.log = (options.log ?? logger).child({ component: "planner", model: options.model });
1645
+ }
1646
+ options;
1647
+ name = "model";
1648
+ log;
1649
+ async plan(request) {
1650
+ const context = buildPlannerContext(request);
1651
+ const messages = [
1652
+ {
1653
+ role: "system",
1654
+ 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 ?? "")
1655
+ },
1656
+ { role: "user", content: context }
1657
+ ];
1658
+ let responseText;
1659
+ try {
1660
+ responseText = await this.options.chat(this.options.model, messages);
1661
+ } catch (error) {
1662
+ return this.degrade(request, `planner model call failed: ${describe(error)}`);
1663
+ }
1664
+ const parsed = parsePlanJson(responseText);
1665
+ if (!parsed) {
1666
+ return this.degrade(request, "planner model output was not valid plan JSON");
1667
+ }
1668
+ const plan = {
1669
+ id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
1670
+ objective: request.objective,
1671
+ summary: parsed.summary,
1672
+ steps: parsed.steps,
1673
+ ...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
1674
+ ...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
1675
+ generatedBy: `model:${this.options.model}`,
1676
+ model: this.options.model,
1677
+ ...request.taskId !== void 0 ? { taskId: request.taskId } : {},
1678
+ ...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
1679
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1680
+ };
1681
+ return plan;
1682
+ }
1683
+ async degrade(request, reason) {
1684
+ if (this.options.fallback) {
1685
+ this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
1686
+ return this.options.fallback.plan(request);
1687
+ }
1688
+ throw new PlannerError("PLANNER_FAILED", `Planner failed: ${reason}.`);
1689
+ }
1690
+ };
1691
+ function parsePlanJson(text) {
1692
+ const unfenced = text.replace(/```(?:json)?/gi, "");
1693
+ const start = unfenced.indexOf("{");
1694
+ const end = unfenced.lastIndexOf("}");
1695
+ if (start < 0 || end <= start) return void 0;
1696
+ try {
1697
+ const json = JSON.parse(unfenced.slice(start, end + 1));
1698
+ const parsed = planJsonSchema.safeParse(json);
1699
+ if (!parsed.success) return void 0;
1700
+ const steps = parsed.data.steps.map((step, index) => ({
1701
+ id: step.id ?? `step-${index + 1}`,
1702
+ title: step.title,
1703
+ description: step.description,
1704
+ order: step.order ?? index + 1,
1705
+ targets: step.targets,
1706
+ acceptanceCriteria: step.acceptance_criteria,
1707
+ dependsOn: step.depends_on,
1708
+ sourceFindings: step.source_findings
1709
+ }));
1710
+ return {
1711
+ summary: parsed.data.summary,
1712
+ steps: steps.sort((a, b) => a.order - b.order),
1713
+ risks: parsed.data.risks,
1714
+ notes: parsed.data.notes
1715
+ };
1716
+ } catch {
1717
+ return void 0;
1718
+ }
1719
+ }
1720
+ function describe(error) {
1721
+ return error instanceof Error ? error.message : String(error);
1722
+ }
1723
+
1724
+ // src/pipelines/engine.ts
1483
1725
  var PipelineEngine = class {
1484
1726
  constructor(options) {
1485
1727
  this.options = options;
@@ -1592,7 +1834,7 @@ var PipelineEngine = class {
1592
1834
  }
1593
1835
  const now = (/* @__PURE__ */ new Date()).toISOString();
1594
1836
  const run = {
1595
- id: `run-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
1837
+ id: `run-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
1596
1838
  pipelineId: def.id,
1597
1839
  status: "running",
1598
1840
  startedAt: now,
@@ -1620,7 +1862,9 @@ var PipelineEngine = class {
1620
1862
  const ctx = {
1621
1863
  objective,
1622
1864
  ...role ? { role } : {},
1623
- combinedContext: contextParts.join("\n\n")
1865
+ combinedContext: contextParts.join("\n\n"),
1866
+ reports: [...allReports],
1867
+ pipelineRunId: run.id
1624
1868
  };
1625
1869
  return this.executeStep(step, ctx);
1626
1870
  })
@@ -1639,6 +1883,7 @@ ${contribution}`);
1639
1883
  stepId: step.id,
1640
1884
  status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
1641
1885
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
1886
+ ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
1642
1887
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
1643
1888
  };
1644
1889
  run.stepRuns.push(stepRun);
@@ -1649,6 +1894,7 @@ ${contribution}`);
1649
1894
  }
1650
1895
  const failed = [...outcomes.values()].some((o) => o.status === "failed");
1651
1896
  const finalStatus = failed ? "failed" : "completed";
1897
+ const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
1652
1898
  let combinedReportId;
1653
1899
  if (this.options.reports) {
1654
1900
  const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
@@ -1670,7 +1916,8 @@ ${contribution}`);
1670
1916
  status: finalStatus,
1671
1917
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1672
1918
  ...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
1673
- ...combinedReportId !== void 0 ? { combinedReportId } : {}
1919
+ ...combinedReportId !== void 0 ? { combinedReportId } : {},
1920
+ ...planId !== void 0 ? { planId } : {}
1674
1921
  };
1675
1922
  await this.options.store.save(finished);
1676
1923
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
@@ -1780,33 +2027,34 @@ ${contribution}`);
1780
2027
  };
1781
2028
  }
1782
2029
  async runPlan(step, ctx) {
1783
- if (!step.model) {
1784
- return { status: "completed", summary: "(plan: no model configured \u2014 skipped)", taskIds: [], reports: [] };
1785
- }
1786
- const messages = [
1787
- {
1788
- role: "system",
1789
- content: "You are a planning assistant inside OmniNode. Given the objective and the combined research context, produce a prioritized, actionable plan."
1790
- },
1791
- {
1792
- role: "user",
1793
- content: `# Objective
1794
- ${ctx.objective}
1795
-
1796
- ${ctx.combinedContext}
1797
-
1798
- Produce the final plan.`
1799
- }
1800
- ];
1801
- try {
1802
- const planText = await this.getChat()(step.model, messages);
2030
+ const request = {
2031
+ objective: ctx.objective,
2032
+ reports: ctx.reports,
2033
+ ...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
2034
+ ...ctx.role ? { role: ctx.role } : {},
2035
+ ...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
2036
+ };
2037
+ let planner;
2038
+ if (step.model) {
2039
+ planner = new ModelPlanner({
2040
+ model: step.model,
2041
+ chat: this.getChat(),
2042
+ fallback: new HeuristicPlanner(),
2043
+ log: this.log
2044
+ });
2045
+ } else if (this.options.planner) {
2046
+ planner = this.options.planner;
2047
+ } else {
1803
2048
  return {
1804
2049
  status: "completed",
1805
- summary: planText.split("\n", 1)[0] ?? planText,
1806
- contextContribution: planText,
2050
+ summary: "(plan: no model or planner configured \u2014 skipped)",
1807
2051
  taskIds: [],
1808
2052
  reports: []
1809
2053
  };
2054
+ }
2055
+ let plan;
2056
+ try {
2057
+ plan = await planner.plan(request);
1810
2058
  } catch (error) {
1811
2059
  return {
1812
2060
  status: "failed",
@@ -1816,6 +2064,15 @@ Produce the final plan.`
1816
2064
  reports: []
1817
2065
  };
1818
2066
  }
2067
+ await this.options.plans?.save(plan);
2068
+ return {
2069
+ status: "completed",
2070
+ summary: plan.summary,
2071
+ contextContribution: formatPlan(plan),
2072
+ taskIds: [],
2073
+ planId: plan.id,
2074
+ reports: []
2075
+ };
1819
2076
  }
1820
2077
  getChat() {
1821
2078
  if (this.options.chat) return this.options.chat;
@@ -1825,6 +2082,21 @@ Produce the final plan.`
1825
2082
  );
1826
2083
  }
1827
2084
  };
2085
+ function formatPlan(plan) {
2086
+ const lines = [
2087
+ `# Implementation Plan (by ${plan.generatedBy})`,
2088
+ plan.summary,
2089
+ "Steps:",
2090
+ ...plan.steps.map((step) => {
2091
+ const targets = step.targets && step.targets.length > 0 ? ` [targets: ${step.targets.join(", ")}]` : "";
2092
+ return `${step.order}. ${step.title}${targets}`;
2093
+ })
2094
+ ];
2095
+ if (plan.risks && plan.risks.length > 0) {
2096
+ lines.push("Risks:", ...plan.risks.map((risk) => `- ${risk}`));
2097
+ }
2098
+ return lines.join("\n");
2099
+ }
1828
2100
  function createDefaultChatFn(providers) {
1829
2101
  return async (modelRef, messages) => {
1830
2102
  const separator = modelRef.indexOf(":");
@@ -1905,9 +2177,86 @@ var FilePipelineRunStore = class {
1905
2177
  }
1906
2178
  };
1907
2179
 
2180
+ // src/planner/store.ts
2181
+ import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
2182
+ import { dirname as dirname4 } from "path";
2183
+ var FilePlanStore = class {
2184
+ filePath;
2185
+ queue = Promise.resolve();
2186
+ constructor(directory = `${process.cwd()}/.omninode`) {
2187
+ this.filePath = `${directory}/plans.json`;
2188
+ }
2189
+ async save(plan) {
2190
+ return this.synchronized(async () => {
2191
+ const plans = await this.load();
2192
+ const index = plans.findIndex((p) => p.id === plan.id);
2193
+ if (index >= 0) plans[index] = plan;
2194
+ else plans.push(plan);
2195
+ await this.write(plans);
2196
+ });
2197
+ }
2198
+ async get(id) {
2199
+ return this.synchronized(async () => {
2200
+ const plans = await this.load();
2201
+ return plans.find((p) => p.id === id);
2202
+ });
2203
+ }
2204
+ async list(filter = {}) {
2205
+ return this.synchronized(async () => {
2206
+ const plans = await this.load();
2207
+ return plans.filter((plan) => filter.pipelineRunId ? plan.pipelineRunId === filter.pipelineRunId : true).filter((plan) => filter.taskId ? plan.taskId === filter.taskId : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2208
+ });
2209
+ }
2210
+ synchronized(operation) {
2211
+ const result = this.queue.then(operation, operation);
2212
+ this.queue = result.then(
2213
+ () => void 0,
2214
+ () => void 0
2215
+ );
2216
+ return result;
2217
+ }
2218
+ async load() {
2219
+ try {
2220
+ const raw = await readFile4(this.filePath, "utf8");
2221
+ const parsed = JSON.parse(raw);
2222
+ return Array.isArray(parsed) ? parsed : [];
2223
+ } catch {
2224
+ return [];
2225
+ }
2226
+ }
2227
+ async write(plans) {
2228
+ const tmp = `${this.filePath}.tmp`;
2229
+ await mkdir4(dirname4(this.filePath), { recursive: true });
2230
+ await writeFile4(tmp, `${JSON.stringify(plans, null, 2)}
2231
+ `, "utf8");
2232
+ await rename4(tmp, this.filePath);
2233
+ }
2234
+ };
2235
+
2236
+ // src/planner/factory.ts
2237
+ function buildPlanner(plannerConfig, chat, log = logger) {
2238
+ if (!plannerConfig) return void 0;
2239
+ if (plannerConfig.kind === "heuristic") {
2240
+ return new HeuristicPlanner();
2241
+ }
2242
+ if (!plannerConfig.model) {
2243
+ throw new ConfigError(
2244
+ "CONFIG_INVALID",
2245
+ 'Planner kind "model" needs a model reference ("provider:model-id") in omninode.yaml.'
2246
+ );
2247
+ }
2248
+ return new ModelPlanner({
2249
+ model: plannerConfig.model,
2250
+ chat,
2251
+ ...plannerConfig.instruction !== void 0 ? { instruction: plannerConfig.instruction } : {},
2252
+ fallback: new HeuristicPlanner(),
2253
+ log
2254
+ });
2255
+ }
2256
+
1908
2257
  // src/reports/aggregate.ts
1909
- import { randomBytes as randomBytes3 } from "crypto";
1910
- var SEVERITY_RANK = {
2258
+ import { randomBytes as randomBytes5 } from "crypto";
2259
+ var SEVERITY_RANK2 = {
1911
2260
  info: 0,
1912
2261
  low: 1,
1913
2262
  medium: 2,
@@ -1930,8 +2279,8 @@ function aggregateReports(reports, options = {}) {
1930
2279
  const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
1931
2280
  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).`;
1932
2281
  return {
1933
- id: `combined-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
1934
- title: options.objective !== void 0 ? truncate(options.objective, 120) : "Combined report",
2282
+ id: `combined-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
2283
+ title: options.objective !== void 0 ? truncate2(options.objective, 120) : "Combined report",
1935
2284
  ...options.objective !== void 0 ? { objective: options.objective } : {},
1936
2285
  ...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
1937
2286
  taskIds: options.taskIds ?? [],
@@ -1944,7 +2293,7 @@ function aggregateReports(reports, options = {}) {
1944
2293
  };
1945
2294
  }
1946
2295
  function rank(finding) {
1947
- return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
2296
+ return finding.severity ? SEVERITY_RANK2[finding.severity] : -1;
1948
2297
  }
1949
2298
  function mergeFinding(groups, finding, report, threshold) {
1950
2299
  const source = { agent: report.agent, reportId: report.id };
@@ -1954,7 +2303,7 @@ function mergeFinding(groups, finding, report, threshold) {
1954
2303
  group.details.push(finding.detail);
1955
2304
  group.sources.push(source);
1956
2305
  group.evidence.push(...finding.evidence ?? []);
1957
- if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
2306
+ if (finding.severity && (!group.severity || SEVERITY_RANK2[finding.severity] > SEVERITY_RANK2[group.severity])) {
1958
2307
  group.severity = finding.severity;
1959
2308
  }
1960
2309
  return;
@@ -1994,7 +2343,7 @@ function tokens(text) {
1994
2343
  text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
1995
2344
  );
1996
2345
  }
1997
- function truncate(text, max) {
2346
+ function truncate2(text, max) {
1998
2347
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1999
2348
  }
2000
2349
 
@@ -2038,14 +2387,14 @@ function extractReportFromText(raw, meta) {
2038
2387
  const nonEmpty = trimmed.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
2039
2388
  const proseLine = nonEmpty.find((line) => !line.startsWith("#") && !BULLET.test(line)) ?? nonEmpty[0] ?? "";
2040
2389
  const cleaned = proseLine.replace(/^#{1,6}\s+/, "").replace(BULLET, "$1");
2041
- const summary = cleaned.length > 0 ? truncate2(cleaned, 200) : "(no output)";
2390
+ const summary = cleaned.length > 0 ? truncate3(cleaned, 200) : "(no output)";
2042
2391
  const sections = parseSections(trimmed);
2043
2392
  const findingTexts = collectBullets(sections, [/finding/, /issue/, /problem/, /vulnerab/]);
2044
2393
  const recommendationTexts = collectBullets(sections, [/recommend/, /suggestion/, /next step/, /action/]);
2045
2394
  const evidenceTexts = collectBullets(sections, [/evidence/, /proof/, /log/]);
2046
2395
  const findings = findingTexts.map((text, index) => ({
2047
2396
  id: `${meta.taskId}-f-${index + 1}`,
2048
- title: truncate2(text, 120),
2397
+ title: truncate3(text, 120),
2049
2398
  detail: text,
2050
2399
  severity: guessSeverity(text)
2051
2400
  }));
@@ -2056,12 +2405,12 @@ function extractReportFromText(raw, meta) {
2056
2405
  ...meta.model !== void 0 ? { model: meta.model } : {},
2057
2406
  summary,
2058
2407
  findings,
2059
- recommendations: recommendationTexts.map((t) => truncate2(t, 200)),
2408
+ recommendations: recommendationTexts.map((t) => truncate3(t, 200)),
2060
2409
  metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
2061
2410
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2062
2411
  };
2063
2412
  }
2064
- function truncate2(text, max) {
2413
+ function truncate3(text, max) {
2065
2414
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
2066
2415
  }
2067
2416
 
@@ -2116,8 +2465,8 @@ var ReportService = class {
2116
2465
  };
2117
2466
 
2118
2467
  // src/reports/store.ts
2119
- import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
2120
- import { dirname as dirname4 } from "path";
2468
+ import { mkdir as mkdir5, readFile as readFile5, rename as rename5, writeFile as writeFile5 } from "fs/promises";
2469
+ import { dirname as dirname5 } from "path";
2121
2470
  var FileReportStore = class {
2122
2471
  filePath;
2123
2472
  queue = Promise.resolve();
@@ -2181,7 +2530,7 @@ var FileReportStore = class {
2181
2530
  }
2182
2531
  async load() {
2183
2532
  try {
2184
- const raw = await readFile4(this.filePath, "utf8");
2533
+ const raw = await readFile5(this.filePath, "utf8");
2185
2534
  const parsed = JSON.parse(raw);
2186
2535
  return Array.isArray(parsed) ? parsed : [];
2187
2536
  } catch {
@@ -2190,10 +2539,10 @@ var FileReportStore = class {
2190
2539
  }
2191
2540
  async write(entries) {
2192
2541
  const tmp = `${this.filePath}.tmp`;
2193
- await mkdir4(dirname4(this.filePath), { recursive: true });
2194
- await writeFile4(tmp, `${JSON.stringify(entries, null, 2)}
2542
+ await mkdir5(dirname5(this.filePath), { recursive: true });
2543
+ await writeFile5(tmp, `${JSON.stringify(entries, null, 2)}
2195
2544
  `, "utf8");
2196
- await rename4(tmp, this.filePath);
2545
+ await rename5(tmp, this.filePath);
2197
2546
  }
2198
2547
  };
2199
2548
 
@@ -2201,6 +2550,7 @@ var FileReportStore = class {
2201
2550
  function buildPipelineEngine(config, log = logger) {
2202
2551
  const agents = buildAgentRegistry(config, log);
2203
2552
  const roles = buildRoleRegistry(config);
2553
+ const chat = createDefaultChatFn(config.project.providers);
2204
2554
  const tasks = new TaskEngine({
2205
2555
  agents,
2206
2556
  roles,
@@ -2214,14 +2564,16 @@ function buildPipelineEngine(config, log = logger) {
2214
2564
  roles,
2215
2565
  providers: config.project.providers,
2216
2566
  store: new FilePipelineRunStore(),
2217
- chat: createDefaultChatFn(config.project.providers),
2567
+ chat,
2568
+ ...config.project.planner !== void 0 ? { planner: buildPlanner(config.project.planner, chat, log) } : {},
2569
+ plans: new FilePlanStore(),
2218
2570
  reports: new ReportService(new FileReportStore(), log),
2219
2571
  log
2220
2572
  });
2221
2573
  }
2222
2574
 
2223
2575
  // src/version.ts
2224
- var OMNINODE_VERSION = "0.8.0";
2576
+ var OMNINODE_VERSION = "0.9.0";
2225
2577
  export {
2226
2578
  AgentError,
2227
2579
  AgentRegistry,
@@ -2242,6 +2594,7 @@ export {
2242
2594
  OpenAICompatibleProvider,
2243
2595
  PipelineEngine,
2244
2596
  PipelineError,
2597
+ PlannerError,
2245
2598
  ProcessAgent,
2246
2599
  ProtocolError,
2247
2600
  ProviderError,
@@ -2273,6 +2626,7 @@ export {
2273
2626
  logger,
2274
2627
  pipelineConfigSchema,
2275
2628
  pipelineStepConfigSchema,
2629
+ plannerConfigSchema,
2276
2630
  projectConfigSchema,
2277
2631
  providerConfigSchema,
2278
2632
  registryKey,