@hilbras/omninode 0.8.0 → 0.10.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,
@@ -1142,6 +1160,25 @@ var TaskEngine = class {
1142
1160
  }
1143
1161
  return this.transition(task, "cancelled");
1144
1162
  }
1163
+ /** Error recovery (§27 Phase 9): reset a failed/cancelled task so it can run again. */
1164
+ async retry(id) {
1165
+ const task = await this.mustGet(id);
1166
+ if (task.status !== "failed" && task.status !== "cancelled") {
1167
+ throw new TaskError(
1168
+ "TASK_INVALID",
1169
+ `Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1170
+ );
1171
+ }
1172
+ const { result: _discarded, ...rest } = task;
1173
+ const reset = {
1174
+ ...rest,
1175
+ status: "created",
1176
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1177
+ };
1178
+ await this.options.store.save(reset);
1179
+ this.log.info(`Task ${id} reset for retry.`);
1180
+ return reset;
1181
+ }
1145
1182
  async get(id) {
1146
1183
  return this.options.store.get(id);
1147
1184
  }
@@ -1479,7 +1516,231 @@ function createTaskEngine(config, log = logger, directory) {
1479
1516
  }
1480
1517
 
1481
1518
  // src/pipelines/engine.ts
1519
+ import { randomBytes as randomBytes4 } from "crypto";
1520
+
1521
+ // src/planner/heuristic.ts
1482
1522
  import { randomBytes as randomBytes2 } from "crypto";
1523
+ var SEVERITY_RANK = {
1524
+ info: 0,
1525
+ low: 1,
1526
+ medium: 2,
1527
+ high: 3,
1528
+ critical: 4
1529
+ };
1530
+ var HeuristicPlanner = class {
1531
+ name = "heuristic";
1532
+ async plan(request) {
1533
+ const steps = [];
1534
+ const findings = request.reports.flatMap(
1535
+ (report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
1536
+ );
1537
+ const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
1538
+ for (const [index, text] of recommendations.entries()) {
1539
+ steps.push({
1540
+ id: `step-${steps.length + 1}`,
1541
+ title: truncate(text, 120),
1542
+ order: steps.length + 1,
1543
+ ...index > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
1544
+ });
1545
+ }
1546
+ 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"]);
1547
+ for (const { finding } of significant) {
1548
+ steps.push(remediationStep(finding, steps.length + 1));
1549
+ }
1550
+ if (steps.length === 0) {
1551
+ steps.push({
1552
+ id: "step-1",
1553
+ title: "Gather additional information",
1554
+ description: "The research reports contained no actionable recommendations or significant findings. Investigate the objective further before planning implementation work.",
1555
+ order: 1
1556
+ });
1557
+ }
1558
+ const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
1559
+ return {
1560
+ id: `plan-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
1561
+ objective: request.objective,
1562
+ summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
1563
+ steps,
1564
+ ...risks.length > 0 ? { risks } : {},
1565
+ generatedBy: "heuristic",
1566
+ ...request.taskId !== void 0 ? { taskId: request.taskId } : {},
1567
+ ...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
1568
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1569
+ };
1570
+ }
1571
+ };
1572
+ function remediationStep(finding, order) {
1573
+ return {
1574
+ id: `step-${order}`,
1575
+ title: `Fix: ${truncate(finding.title, 100)}`,
1576
+ description: finding.detail,
1577
+ order,
1578
+ dependsOn: order > 1 ? [`step-${order - 1}`] : [],
1579
+ sourceFindings: [finding.id]
1580
+ };
1581
+ }
1582
+ function dedupe(texts) {
1583
+ return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
1584
+ }
1585
+ function truncate(text, max) {
1586
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1587
+ }
1588
+
1589
+ // src/planner/model.ts
1590
+ import { randomBytes as randomBytes3 } from "crypto";
1591
+ import { z as z2 } from "zod";
1592
+
1593
+ // src/planner/context.ts
1594
+ function buildPlannerContext(request) {
1595
+ const parts = [`# Objective
1596
+ ${request.objective}`];
1597
+ if (request.role) {
1598
+ const roleLines = [`# Role
1599
+ ${request.role.name} (${request.role.id})`];
1600
+ if (request.role.responsibilities.length > 0) {
1601
+ roleLines.push("Responsibilities:", ...request.role.responsibilities.map((r) => `- ${r}`));
1602
+ }
1603
+ if (request.role.expectedOutputs.length > 0) {
1604
+ roleLines.push("Expected outputs:", ...request.role.expectedOutputs.map((o) => `- ${o}`));
1605
+ }
1606
+ parts.push(roleLines.join("\n"));
1607
+ }
1608
+ if (request.reports.length > 0) {
1609
+ const agents = [...new Set(request.reports.map((r) => r.agent))].join(", ");
1610
+ const findingLines = request.reports.flatMap(
1611
+ (report) => report.findings.map(
1612
+ (finding) => `- [${finding.severity ?? "info"}] ${finding.title} (source: ${report.agent}, finding: ${finding.id})` + (finding.detail ? ` \u2014 ${finding.detail}` : "")
1613
+ )
1614
+ );
1615
+ parts.push(
1616
+ `# Findings (${request.reports.length} report(s) by ${agents})
1617
+ ` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
1618
+ );
1619
+ const recommendationLines = request.reports.flatMap(
1620
+ (report) => report.recommendations.map((text) => `- ${text} (source: ${report.agent})`)
1621
+ );
1622
+ if (recommendationLines.length > 0) {
1623
+ parts.push(`# Recommendations
1624
+ ${recommendationLines.join("\n")}`);
1625
+ }
1626
+ const summaryLines = request.reports.map((report) => `- [${report.agent}] ${report.summary}`);
1627
+ parts.push(`# Report summaries
1628
+ ${summaryLines.join("\n")}`);
1629
+ }
1630
+ if (request.memory && request.memory.length > 0) {
1631
+ parts.push(
1632
+ `# Relevant memory
1633
+ ` + request.memory.map((entry) => `- [${entry.scope}] ${entry.content}`).join("\n")
1634
+ );
1635
+ }
1636
+ if (request.context && request.context.length > 0) {
1637
+ parts.push(`# Research context
1638
+ ${request.context}`);
1639
+ }
1640
+ return parts.join("\n\n");
1641
+ }
1642
+
1643
+ // src/planner/model.ts
1644
+ var planStepJsonSchema = z2.object({
1645
+ id: z2.string().optional(),
1646
+ title: z2.string().min(1),
1647
+ description: z2.string().optional(),
1648
+ order: z2.number().int().optional(),
1649
+ targets: z2.array(z2.string()).optional(),
1650
+ acceptance_criteria: z2.array(z2.string()).optional(),
1651
+ depends_on: z2.array(z2.string()).optional(),
1652
+ source_findings: z2.array(z2.string()).optional()
1653
+ }).strict();
1654
+ var planJsonSchema = z2.object({
1655
+ summary: z2.string().min(1),
1656
+ steps: z2.array(planStepJsonSchema).min(1),
1657
+ risks: z2.array(z2.string()).optional(),
1658
+ notes: z2.array(z2.string()).optional()
1659
+ }).strict();
1660
+ var ModelPlanner = class {
1661
+ constructor(options) {
1662
+ this.options = options;
1663
+ this.log = (options.log ?? logger).child({ component: "planner", model: options.model });
1664
+ }
1665
+ options;
1666
+ name = "model";
1667
+ log;
1668
+ async plan(request) {
1669
+ const context = buildPlannerContext(request);
1670
+ const messages = [
1671
+ {
1672
+ role: "system",
1673
+ 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 ?? "")
1674
+ },
1675
+ { role: "user", content: context }
1676
+ ];
1677
+ let responseText;
1678
+ try {
1679
+ responseText = await this.options.chat(this.options.model, messages);
1680
+ } catch (error) {
1681
+ return this.degrade(request, `planner model call failed: ${describe(error)}`);
1682
+ }
1683
+ const parsed = parsePlanJson(responseText);
1684
+ if (!parsed) {
1685
+ return this.degrade(request, "planner model output was not valid plan JSON");
1686
+ }
1687
+ const plan = {
1688
+ id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
1689
+ objective: request.objective,
1690
+ summary: parsed.summary,
1691
+ steps: parsed.steps,
1692
+ ...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
1693
+ ...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
1694
+ generatedBy: `model:${this.options.model}`,
1695
+ model: this.options.model,
1696
+ ...request.taskId !== void 0 ? { taskId: request.taskId } : {},
1697
+ ...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
1698
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1699
+ };
1700
+ return plan;
1701
+ }
1702
+ async degrade(request, reason) {
1703
+ if (this.options.fallback) {
1704
+ this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
1705
+ return this.options.fallback.plan(request);
1706
+ }
1707
+ throw new PlannerError("PLANNER_FAILED", `Planner failed: ${reason}.`);
1708
+ }
1709
+ };
1710
+ function parsePlanJson(text) {
1711
+ const unfenced = text.replace(/```(?:json)?/gi, "");
1712
+ const start = unfenced.indexOf("{");
1713
+ const end = unfenced.lastIndexOf("}");
1714
+ if (start < 0 || end <= start) return void 0;
1715
+ try {
1716
+ const json = JSON.parse(unfenced.slice(start, end + 1));
1717
+ const parsed = planJsonSchema.safeParse(json);
1718
+ if (!parsed.success) return void 0;
1719
+ const steps = parsed.data.steps.map((step, index) => ({
1720
+ id: step.id ?? `step-${index + 1}`,
1721
+ title: step.title,
1722
+ description: step.description,
1723
+ order: step.order ?? index + 1,
1724
+ targets: step.targets,
1725
+ acceptanceCriteria: step.acceptance_criteria,
1726
+ dependsOn: step.depends_on,
1727
+ sourceFindings: step.source_findings
1728
+ }));
1729
+ return {
1730
+ summary: parsed.data.summary,
1731
+ steps: steps.sort((a, b) => a.order - b.order),
1732
+ risks: parsed.data.risks,
1733
+ notes: parsed.data.notes
1734
+ };
1735
+ } catch {
1736
+ return void 0;
1737
+ }
1738
+ }
1739
+ function describe(error) {
1740
+ return error instanceof Error ? error.message : String(error);
1741
+ }
1742
+
1743
+ // src/pipelines/engine.ts
1483
1744
  var PipelineEngine = class {
1484
1745
  constructor(options) {
1485
1746
  this.options = options;
@@ -1592,8 +1853,9 @@ var PipelineEngine = class {
1592
1853
  }
1593
1854
  const now = (/* @__PURE__ */ new Date()).toISOString();
1594
1855
  const run = {
1595
- id: `run-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
1856
+ id: `run-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
1596
1857
  pipelineId: def.id,
1858
+ objective,
1597
1859
  status: "running",
1598
1860
  startedAt: now,
1599
1861
  stepRuns: []
@@ -1620,7 +1882,9 @@ var PipelineEngine = class {
1620
1882
  const ctx = {
1621
1883
  objective,
1622
1884
  ...role ? { role } : {},
1623
- combinedContext: contextParts.join("\n\n")
1885
+ combinedContext: contextParts.join("\n\n"),
1886
+ reports: [...allReports],
1887
+ pipelineRunId: run.id
1624
1888
  };
1625
1889
  return this.executeStep(step, ctx);
1626
1890
  })
@@ -1639,6 +1903,7 @@ ${contribution}`);
1639
1903
  stepId: step.id,
1640
1904
  status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
1641
1905
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
1906
+ ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
1642
1907
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
1643
1908
  };
1644
1909
  run.stepRuns.push(stepRun);
@@ -1649,9 +1914,20 @@ ${contribution}`);
1649
1914
  }
1650
1915
  const failed = [...outcomes.values()].some((o) => o.status === "failed");
1651
1916
  const finalStatus = failed ? "failed" : "completed";
1917
+ const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
1918
+ let resultSummary;
1919
+ for (let i = def.steps.length - 1; i >= 0; i -= 1) {
1920
+ const step = def.steps[i];
1921
+ const outcome = outcomes.get(step.id);
1922
+ if (outcome?.status !== "completed" || outcome.taskIds.length === 0) continue;
1923
+ const stepTasks = (await Promise.all(outcome.taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
1924
+ const summaries = stepTasks.map((task) => task.result?.summary).filter((summary) => summary !== void 0);
1925
+ if (summaries.length > 0) resultSummary = summaries.join("\n");
1926
+ break;
1927
+ }
1652
1928
  let combinedReportId;
1653
1929
  if (this.options.reports) {
1654
- const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
1930
+ const taskIds = def.steps.filter((step) => step.kind === "research" || step.kind === "analyze").flatMap((step) => outcomes.get(step.id)?.taskIds ?? []);
1655
1931
  const tasks = (await Promise.all(taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
1656
1932
  const collected = await this.options.reports.collectFromTasks(tasks);
1657
1933
  await this.options.reports.saveReports(collected);
@@ -1670,7 +1946,9 @@ ${contribution}`);
1670
1946
  status: finalStatus,
1671
1947
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1672
1948
  ...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
1673
- ...combinedReportId !== void 0 ? { combinedReportId } : {}
1949
+ ...combinedReportId !== void 0 ? { combinedReportId } : {},
1950
+ ...planId !== void 0 ? { planId } : {},
1951
+ ...resultSummary !== void 0 ? { resultSummary } : {}
1674
1952
  };
1675
1953
  await this.options.store.save(finished);
1676
1954
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
@@ -1780,33 +2058,34 @@ ${contribution}`);
1780
2058
  };
1781
2059
  }
1782
2060
  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);
2061
+ const request = {
2062
+ objective: ctx.objective,
2063
+ reports: ctx.reports,
2064
+ ...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
2065
+ ...ctx.role ? { role: ctx.role } : {},
2066
+ ...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
2067
+ };
2068
+ let planner;
2069
+ if (step.model) {
2070
+ planner = new ModelPlanner({
2071
+ model: step.model,
2072
+ chat: this.getChat(),
2073
+ fallback: new HeuristicPlanner(),
2074
+ log: this.log
2075
+ });
2076
+ } else if (this.options.planner) {
2077
+ planner = this.options.planner;
2078
+ } else {
1803
2079
  return {
1804
2080
  status: "completed",
1805
- summary: planText.split("\n", 1)[0] ?? planText,
1806
- contextContribution: planText,
2081
+ summary: "(plan: no model or planner configured \u2014 skipped)",
1807
2082
  taskIds: [],
1808
2083
  reports: []
1809
2084
  };
2085
+ }
2086
+ let plan;
2087
+ try {
2088
+ plan = await planner.plan(request);
1810
2089
  } catch (error) {
1811
2090
  return {
1812
2091
  status: "failed",
@@ -1816,6 +2095,15 @@ Produce the final plan.`
1816
2095
  reports: []
1817
2096
  };
1818
2097
  }
2098
+ await this.options.plans?.save(plan);
2099
+ return {
2100
+ status: "completed",
2101
+ summary: plan.summary,
2102
+ contextContribution: formatPlan(plan),
2103
+ taskIds: [],
2104
+ planId: plan.id,
2105
+ reports: []
2106
+ };
1819
2107
  }
1820
2108
  getChat() {
1821
2109
  if (this.options.chat) return this.options.chat;
@@ -1825,6 +2113,21 @@ Produce the final plan.`
1825
2113
  );
1826
2114
  }
1827
2115
  };
2116
+ function formatPlan(plan) {
2117
+ const lines = [
2118
+ `# Implementation Plan (by ${plan.generatedBy})`,
2119
+ plan.summary,
2120
+ "Steps:",
2121
+ ...plan.steps.map((step) => {
2122
+ const targets = step.targets && step.targets.length > 0 ? ` [targets: ${step.targets.join(", ")}]` : "";
2123
+ return `${step.order}. ${step.title}${targets}`;
2124
+ })
2125
+ ];
2126
+ if (plan.risks && plan.risks.length > 0) {
2127
+ lines.push("Risks:", ...plan.risks.map((risk) => `- ${risk}`));
2128
+ }
2129
+ return lines.join("\n");
2130
+ }
1828
2131
  function createDefaultChatFn(providers) {
1829
2132
  return async (modelRef, messages) => {
1830
2133
  const separator = modelRef.indexOf(":");
@@ -1905,9 +2208,86 @@ var FilePipelineRunStore = class {
1905
2208
  }
1906
2209
  };
1907
2210
 
2211
+ // src/planner/store.ts
2212
+ import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
2213
+ import { dirname as dirname4 } from "path";
2214
+ var FilePlanStore = class {
2215
+ filePath;
2216
+ queue = Promise.resolve();
2217
+ constructor(directory = `${process.cwd()}/.omninode`) {
2218
+ this.filePath = `${directory}/plans.json`;
2219
+ }
2220
+ async save(plan) {
2221
+ return this.synchronized(async () => {
2222
+ const plans = await this.load();
2223
+ const index = plans.findIndex((p) => p.id === plan.id);
2224
+ if (index >= 0) plans[index] = plan;
2225
+ else plans.push(plan);
2226
+ await this.write(plans);
2227
+ });
2228
+ }
2229
+ async get(id) {
2230
+ return this.synchronized(async () => {
2231
+ const plans = await this.load();
2232
+ return plans.find((p) => p.id === id);
2233
+ });
2234
+ }
2235
+ async list(filter = {}) {
2236
+ return this.synchronized(async () => {
2237
+ const plans = await this.load();
2238
+ 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));
2239
+ });
2240
+ }
2241
+ synchronized(operation) {
2242
+ const result = this.queue.then(operation, operation);
2243
+ this.queue = result.then(
2244
+ () => void 0,
2245
+ () => void 0
2246
+ );
2247
+ return result;
2248
+ }
2249
+ async load() {
2250
+ try {
2251
+ const raw = await readFile4(this.filePath, "utf8");
2252
+ const parsed = JSON.parse(raw);
2253
+ return Array.isArray(parsed) ? parsed : [];
2254
+ } catch {
2255
+ return [];
2256
+ }
2257
+ }
2258
+ async write(plans) {
2259
+ const tmp = `${this.filePath}.tmp`;
2260
+ await mkdir4(dirname4(this.filePath), { recursive: true });
2261
+ await writeFile4(tmp, `${JSON.stringify(plans, null, 2)}
2262
+ `, "utf8");
2263
+ await rename4(tmp, this.filePath);
2264
+ }
2265
+ };
2266
+
2267
+ // src/planner/factory.ts
2268
+ function buildPlanner(plannerConfig, chat, log = logger) {
2269
+ if (!plannerConfig) return void 0;
2270
+ if (plannerConfig.kind === "heuristic") {
2271
+ return new HeuristicPlanner();
2272
+ }
2273
+ if (!plannerConfig.model) {
2274
+ throw new ConfigError(
2275
+ "CONFIG_INVALID",
2276
+ 'Planner kind "model" needs a model reference ("provider:model-id") in omninode.yaml.'
2277
+ );
2278
+ }
2279
+ return new ModelPlanner({
2280
+ model: plannerConfig.model,
2281
+ chat,
2282
+ ...plannerConfig.instruction !== void 0 ? { instruction: plannerConfig.instruction } : {},
2283
+ fallback: new HeuristicPlanner(),
2284
+ log
2285
+ });
2286
+ }
2287
+
1908
2288
  // src/reports/aggregate.ts
1909
- import { randomBytes as randomBytes3 } from "crypto";
1910
- var SEVERITY_RANK = {
2289
+ import { randomBytes as randomBytes5 } from "crypto";
2290
+ var SEVERITY_RANK2 = {
1911
2291
  info: 0,
1912
2292
  low: 1,
1913
2293
  medium: 2,
@@ -1930,8 +2310,8 @@ function aggregateReports(reports, options = {}) {
1930
2310
  const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
1931
2311
  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
2312
  return {
1933
- id: `combined-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
1934
- title: options.objective !== void 0 ? truncate(options.objective, 120) : "Combined report",
2313
+ id: `combined-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
2314
+ title: options.objective !== void 0 ? truncate2(options.objective, 120) : "Combined report",
1935
2315
  ...options.objective !== void 0 ? { objective: options.objective } : {},
1936
2316
  ...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
1937
2317
  taskIds: options.taskIds ?? [],
@@ -1944,7 +2324,7 @@ function aggregateReports(reports, options = {}) {
1944
2324
  };
1945
2325
  }
1946
2326
  function rank(finding) {
1947
- return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
2327
+ return finding.severity ? SEVERITY_RANK2[finding.severity] : -1;
1948
2328
  }
1949
2329
  function mergeFinding(groups, finding, report, threshold) {
1950
2330
  const source = { agent: report.agent, reportId: report.id };
@@ -1954,7 +2334,7 @@ function mergeFinding(groups, finding, report, threshold) {
1954
2334
  group.details.push(finding.detail);
1955
2335
  group.sources.push(source);
1956
2336
  group.evidence.push(...finding.evidence ?? []);
1957
- if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
2337
+ if (finding.severity && (!group.severity || SEVERITY_RANK2[finding.severity] > SEVERITY_RANK2[group.severity])) {
1958
2338
  group.severity = finding.severity;
1959
2339
  }
1960
2340
  return;
@@ -1994,7 +2374,7 @@ function tokens(text) {
1994
2374
  text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
1995
2375
  );
1996
2376
  }
1997
- function truncate(text, max) {
2377
+ function truncate2(text, max) {
1998
2378
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1999
2379
  }
2000
2380
 
@@ -2038,14 +2418,14 @@ function extractReportFromText(raw, meta) {
2038
2418
  const nonEmpty = trimmed.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
2039
2419
  const proseLine = nonEmpty.find((line) => !line.startsWith("#") && !BULLET.test(line)) ?? nonEmpty[0] ?? "";
2040
2420
  const cleaned = proseLine.replace(/^#{1,6}\s+/, "").replace(BULLET, "$1");
2041
- const summary = cleaned.length > 0 ? truncate2(cleaned, 200) : "(no output)";
2421
+ const summary = cleaned.length > 0 ? truncate3(cleaned, 200) : "(no output)";
2042
2422
  const sections = parseSections(trimmed);
2043
2423
  const findingTexts = collectBullets(sections, [/finding/, /issue/, /problem/, /vulnerab/]);
2044
2424
  const recommendationTexts = collectBullets(sections, [/recommend/, /suggestion/, /next step/, /action/]);
2045
2425
  const evidenceTexts = collectBullets(sections, [/evidence/, /proof/, /log/]);
2046
2426
  const findings = findingTexts.map((text, index) => ({
2047
2427
  id: `${meta.taskId}-f-${index + 1}`,
2048
- title: truncate2(text, 120),
2428
+ title: truncate3(text, 120),
2049
2429
  detail: text,
2050
2430
  severity: guessSeverity(text)
2051
2431
  }));
@@ -2056,12 +2436,12 @@ function extractReportFromText(raw, meta) {
2056
2436
  ...meta.model !== void 0 ? { model: meta.model } : {},
2057
2437
  summary,
2058
2438
  findings,
2059
- recommendations: recommendationTexts.map((t) => truncate2(t, 200)),
2439
+ recommendations: recommendationTexts.map((t) => truncate3(t, 200)),
2060
2440
  metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
2061
2441
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2062
2442
  };
2063
2443
  }
2064
- function truncate2(text, max) {
2444
+ function truncate3(text, max) {
2065
2445
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
2066
2446
  }
2067
2447
 
@@ -2116,8 +2496,8 @@ var ReportService = class {
2116
2496
  };
2117
2497
 
2118
2498
  // 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";
2499
+ import { mkdir as mkdir5, readFile as readFile5, rename as rename5, writeFile as writeFile5 } from "fs/promises";
2500
+ import { dirname as dirname5 } from "path";
2121
2501
  var FileReportStore = class {
2122
2502
  filePath;
2123
2503
  queue = Promise.resolve();
@@ -2181,7 +2561,7 @@ var FileReportStore = class {
2181
2561
  }
2182
2562
  async load() {
2183
2563
  try {
2184
- const raw = await readFile4(this.filePath, "utf8");
2564
+ const raw = await readFile5(this.filePath, "utf8");
2185
2565
  const parsed = JSON.parse(raw);
2186
2566
  return Array.isArray(parsed) ? parsed : [];
2187
2567
  } catch {
@@ -2190,10 +2570,10 @@ var FileReportStore = class {
2190
2570
  }
2191
2571
  async write(entries) {
2192
2572
  const tmp = `${this.filePath}.tmp`;
2193
- await mkdir4(dirname4(this.filePath), { recursive: true });
2194
- await writeFile4(tmp, `${JSON.stringify(entries, null, 2)}
2573
+ await mkdir5(dirname5(this.filePath), { recursive: true });
2574
+ await writeFile5(tmp, `${JSON.stringify(entries, null, 2)}
2195
2575
  `, "utf8");
2196
- await rename4(tmp, this.filePath);
2576
+ await rename5(tmp, this.filePath);
2197
2577
  }
2198
2578
  };
2199
2579
 
@@ -2201,6 +2581,7 @@ var FileReportStore = class {
2201
2581
  function buildPipelineEngine(config, log = logger) {
2202
2582
  const agents = buildAgentRegistry(config, log);
2203
2583
  const roles = buildRoleRegistry(config);
2584
+ const chat = createDefaultChatFn(config.project.providers);
2204
2585
  const tasks = new TaskEngine({
2205
2586
  agents,
2206
2587
  roles,
@@ -2214,14 +2595,16 @@ function buildPipelineEngine(config, log = logger) {
2214
2595
  roles,
2215
2596
  providers: config.project.providers,
2216
2597
  store: new FilePipelineRunStore(),
2217
- chat: createDefaultChatFn(config.project.providers),
2598
+ chat,
2599
+ ...config.project.planner !== void 0 ? { planner: buildPlanner(config.project.planner, chat, log) } : {},
2600
+ plans: new FilePlanStore(),
2218
2601
  reports: new ReportService(new FileReportStore(), log),
2219
2602
  log
2220
2603
  });
2221
2604
  }
2222
2605
 
2223
2606
  // src/version.ts
2224
- var OMNINODE_VERSION = "0.8.0";
2607
+ var OMNINODE_VERSION = "0.10.0";
2225
2608
  export {
2226
2609
  AgentError,
2227
2610
  AgentRegistry,
@@ -2242,6 +2625,7 @@ export {
2242
2625
  OpenAICompatibleProvider,
2243
2626
  PipelineEngine,
2244
2627
  PipelineError,
2628
+ PlannerError,
2245
2629
  ProcessAgent,
2246
2630
  ProtocolError,
2247
2631
  ProviderError,
@@ -2273,6 +2657,7 @@ export {
2273
2657
  logger,
2274
2658
  pipelineConfigSchema,
2275
2659
  pipelineStepConfigSchema,
2660
+ plannerConfigSchema,
2276
2661
  projectConfigSchema,
2277
2662
  providerConfigSchema,
2278
2663
  registryKey,