@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/README.md +28 -3
- package/dist/cli/index.js +429 -27
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +148 -5
- package/dist/index.js +398 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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)}-${
|
|
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
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
{
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
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:
|
|
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
|
|
1910
|
-
var
|
|
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)}-${
|
|
1934
|
-
title: options.objective !== void 0 ?
|
|
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 ?
|
|
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 ||
|
|
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
|
|
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 ?
|
|
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:
|
|
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) =>
|
|
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
|
|
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
|
|
2120
|
-
import { dirname as
|
|
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
|
|
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
|
|
2194
|
-
await
|
|
2542
|
+
await mkdir5(dirname5(this.filePath), { recursive: true });
|
|
2543
|
+
await writeFile5(tmp, `${JSON.stringify(entries, null, 2)}
|
|
2195
2544
|
`, "utf8");
|
|
2196
|
-
await
|
|
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
|
|
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.
|
|
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,
|