@hilbras/omninode 0.6.0 → 0.7.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 +12 -3
- package/dist/cli/index.js +413 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +83 -3
- package/dist/index.js +314 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ existing intelligence; it does not try to become another model.
|
|
|
18
18
|
|
|
19
19
|
## Status
|
|
20
20
|
|
|
21
|
-
**v0.
|
|
21
|
+
**v0.7.0 — Phase 6: Multi-AI Report System** (see [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md)
|
|
22
22
|
for the full plan and roadmap).
|
|
23
23
|
|
|
24
24
|
What exists today:
|
|
@@ -66,6 +66,14 @@ What exists today:
|
|
|
66
66
|
- Failure handling: retries per step, `on-success`/`on-failure`/`always`
|
|
67
67
|
conditions, combined context threaded downstream; every task runs through
|
|
68
68
|
the TaskEngine and every run is persisted to `.omninode/pipelines.json`
|
|
69
|
+
- **Multi-AI report system** (`src/reports/`):
|
|
70
|
+
- Report collection from finished tasks — structured protocol reports pass
|
|
71
|
+
through; plain text output is normalized via deterministic extraction
|
|
72
|
+
(findings/recommendations/evidence sections, severity guessing)
|
|
73
|
+
- Persistence in `.omninode/reports.json` with full source attribution
|
|
74
|
+
- Aggregation (§17): duplicate/similar findings merged via deterministic
|
|
75
|
+
token-set similarity, sources preserved, highest severity kept
|
|
76
|
+
- Combined reports generated automatically at the end of every pipeline run
|
|
69
77
|
- Typed error hierarchy (`src/errors/`)
|
|
70
78
|
- Leveled logger with pluggable sink (`src/logger/`)
|
|
71
79
|
- Configuration system: `omninode.yaml` with `${ENV_VAR}` expansion and strict
|
|
@@ -144,6 +152,8 @@ project:
|
|
|
144
152
|
|
|
145
153
|
```bash
|
|
146
154
|
omninode pipeline run repo-audit "Audit the authentication module"
|
|
155
|
+
omninode report combined # the aggregated intelligence of that run
|
|
156
|
+
omninode report show combined-xxx # grouped findings with per-source attribution
|
|
147
157
|
```
|
|
148
158
|
|
|
149
159
|
See [omninode.yaml.example](omninode.yaml.example) for a full configuration.
|
|
@@ -180,8 +190,7 @@ Phases 0–11 are laid out in [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.m
|
|
|
180
190
|
| v0.3.0 | OmniHilbras Integration (optional, like every provider) |
|
|
181
191
|
| v0.4.0 | Agent System |
|
|
182
192
|
| v0.5.0 | Roles & Tasks |
|
|
183
|
-
| **v0.
|
|
184
|
-
| v0.7.0 | Multi-AI Reports |
|
|
193
|
+
| **v0.7.0** | **Multi-AI Reports (current)** |
|
|
185
194
|
| v0.8.0 | Remembera (optional memory integration) |
|
|
186
195
|
| v0.9.0 | Planner |
|
|
187
196
|
| v0.10.0 | End-to-End Runtime |
|
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
|
-
var OMNINODE_VERSION = "0.
|
|
9
|
+
var OMNINODE_VERSION = "0.7.0";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -675,6 +675,9 @@ function notImplemented(command, when) {
|
|
|
675
675
|
`${command} is planned for ${when}. Edit omninode.yaml directly for now.`
|
|
676
676
|
);
|
|
677
677
|
}
|
|
678
|
+
function truncate(text, max) {
|
|
679
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
680
|
+
}
|
|
678
681
|
function appendToConfigList(listKey, item) {
|
|
679
682
|
const configPath = findConfigFile();
|
|
680
683
|
if (!configPath) {
|
|
@@ -1404,6 +1407,8 @@ var TaskEngine = class {
|
|
|
1404
1407
|
const result = {
|
|
1405
1408
|
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
1406
1409
|
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
1410
|
+
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
1411
|
+
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
1407
1412
|
...output.error !== void 0 ? { error: output.error } : {},
|
|
1408
1413
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1409
1414
|
};
|
|
@@ -1538,7 +1543,7 @@ function registerTaskCommands(program) {
|
|
|
1538
1543
|
return;
|
|
1539
1544
|
}
|
|
1540
1545
|
for (const t of tasks) {
|
|
1541
|
-
console.log(`${t.id} ${t.status.padEnd(10)} ${
|
|
1546
|
+
console.log(`${t.id} ${t.status.padEnd(10)} ${truncate2(t.objective, 60)}`);
|
|
1542
1547
|
}
|
|
1543
1548
|
});
|
|
1544
1549
|
task.command("cancel <task-id>").description("Cancel a task that has not finished yet.").action(async (taskId) => {
|
|
@@ -1568,20 +1573,392 @@ function printOutcome(task, options = {}) {
|
|
|
1568
1573
|
if (result.error) console.log(` error: ${result.error}`);
|
|
1569
1574
|
if (result.finishedAt) console.log(` ended: ${result.finishedAt}`);
|
|
1570
1575
|
}
|
|
1571
|
-
function
|
|
1576
|
+
function truncate2(text, max) {
|
|
1577
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// src/reports/aggregate.ts
|
|
1581
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1582
|
+
var SEVERITY_RANK = {
|
|
1583
|
+
info: 0,
|
|
1584
|
+
low: 1,
|
|
1585
|
+
medium: 2,
|
|
1586
|
+
high: 3,
|
|
1587
|
+
critical: 4
|
|
1588
|
+
};
|
|
1589
|
+
function aggregateReports(reports, options = {}) {
|
|
1590
|
+
const threshold = options.similarityThreshold ?? 0.7;
|
|
1591
|
+
const findingGroups = [];
|
|
1592
|
+
const recommendationGroups = [];
|
|
1593
|
+
for (const report of reports) {
|
|
1594
|
+
for (const finding of report.findings) {
|
|
1595
|
+
mergeFinding(findingGroups, finding, report, threshold);
|
|
1596
|
+
}
|
|
1597
|
+
for (const text of report.recommendations) {
|
|
1598
|
+
mergeRecommendation(recommendationGroups, text, report, threshold);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
const sources = [...new Set(reports.map((r) => r.agent))].sort();
|
|
1602
|
+
const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
1603
|
+
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).`;
|
|
1604
|
+
return {
|
|
1605
|
+
id: `combined-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
|
|
1606
|
+
title: options.objective !== void 0 ? truncate3(options.objective, 120) : "Combined report",
|
|
1607
|
+
...options.objective !== void 0 ? { objective: options.objective } : {},
|
|
1608
|
+
...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
|
|
1609
|
+
taskIds: options.taskIds ?? [],
|
|
1610
|
+
summary,
|
|
1611
|
+
findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
|
|
1612
|
+
recommendations: recommendationGroups,
|
|
1613
|
+
sources,
|
|
1614
|
+
reportIds: reports.map((r) => r.id),
|
|
1615
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
function rank(finding) {
|
|
1619
|
+
return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
|
|
1620
|
+
}
|
|
1621
|
+
function mergeFinding(groups, finding, report, threshold) {
|
|
1622
|
+
const source = { agent: report.agent, reportId: report.id };
|
|
1623
|
+
const group = groups.find((g) => similar(g.title, finding.title, threshold));
|
|
1624
|
+
if (group) {
|
|
1625
|
+
group.occurrences += 1;
|
|
1626
|
+
group.details.push(finding.detail);
|
|
1627
|
+
group.sources.push(source);
|
|
1628
|
+
group.evidence.push(...finding.evidence ?? []);
|
|
1629
|
+
if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
|
|
1630
|
+
group.severity = finding.severity;
|
|
1631
|
+
}
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
groups.push({
|
|
1635
|
+
title: finding.title,
|
|
1636
|
+
details: [finding.detail],
|
|
1637
|
+
sources: [source],
|
|
1638
|
+
occurrences: 1,
|
|
1639
|
+
...finding.severity ? { severity: finding.severity } : {},
|
|
1640
|
+
evidence: [...finding.evidence ?? []]
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
function mergeRecommendation(groups, text, report, threshold) {
|
|
1644
|
+
const source = { agent: report.agent, reportId: report.id };
|
|
1645
|
+
const group = groups.find((g) => similar(g.text, text, threshold));
|
|
1646
|
+
if (group) {
|
|
1647
|
+
group.occurrences += 1;
|
|
1648
|
+
group.sources.push(source);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
groups.push({ text, sources: [source], occurrences: 1 });
|
|
1652
|
+
}
|
|
1653
|
+
function similar(a, b, threshold) {
|
|
1654
|
+
const left = tokens(a);
|
|
1655
|
+
const right = tokens(b);
|
|
1656
|
+
if (left.size === 0 || right.size === 0) return false;
|
|
1657
|
+
let intersection = 0;
|
|
1658
|
+
for (const token of left) {
|
|
1659
|
+
if (right.has(token)) intersection += 1;
|
|
1660
|
+
}
|
|
1661
|
+
const union = left.size + right.size - intersection;
|
|
1662
|
+
return intersection / union >= threshold;
|
|
1663
|
+
}
|
|
1664
|
+
function tokens(text) {
|
|
1665
|
+
return new Set(
|
|
1666
|
+
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
function truncate3(text, max) {
|
|
1670
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/reports/extract.ts
|
|
1674
|
+
var BULLET = /^(?:[-*•]|\d+[.)])\s+(.*)$/;
|
|
1675
|
+
function parseSections(raw) {
|
|
1676
|
+
const sections = [];
|
|
1677
|
+
let current = { heading: "", bullets: [] };
|
|
1678
|
+
for (const line of raw.split("\n")) {
|
|
1679
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line.trim());
|
|
1680
|
+
if (heading) {
|
|
1681
|
+
if (current.heading.length > 0 || current.bullets.length > 0) {
|
|
1682
|
+
sections.push(current);
|
|
1683
|
+
}
|
|
1684
|
+
current = { heading: heading[2]?.toLowerCase() ?? "", bullets: [] };
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
const bullet = BULLET.exec(line.trim());
|
|
1688
|
+
if (bullet && current.heading.length > 0) {
|
|
1689
|
+
current.bullets.push(bullet[1]?.trim() ?? "");
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (current.heading.length > 0 || current.bullets.length > 0) {
|
|
1693
|
+
sections.push(current);
|
|
1694
|
+
}
|
|
1695
|
+
return sections;
|
|
1696
|
+
}
|
|
1697
|
+
function collectBullets(sections, patterns) {
|
|
1698
|
+
const matches = sections.filter((section) => patterns.some((p) => p.test(section.heading)));
|
|
1699
|
+
return matches.flatMap((section) => section.bullets).filter((b) => b.length > 0);
|
|
1700
|
+
}
|
|
1701
|
+
function guessSeverity(text) {
|
|
1702
|
+
if (/critical|severe/i.test(text)) return "critical";
|
|
1703
|
+
if (/\bhigh\b|major/i.test(text)) return "high";
|
|
1704
|
+
if (/medium|moderate/i.test(text)) return "medium";
|
|
1705
|
+
if (/\blow\b|minor/i.test(text)) return "low";
|
|
1706
|
+
return void 0;
|
|
1707
|
+
}
|
|
1708
|
+
function extractReportFromText(raw, meta) {
|
|
1709
|
+
const trimmed = raw.trim();
|
|
1710
|
+
const nonEmpty = trimmed.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1711
|
+
const proseLine = nonEmpty.find((line) => !line.startsWith("#") && !BULLET.test(line)) ?? nonEmpty[0] ?? "";
|
|
1712
|
+
const cleaned = proseLine.replace(/^#{1,6}\s+/, "").replace(BULLET, "$1");
|
|
1713
|
+
const summary = cleaned.length > 0 ? truncate4(cleaned, 200) : "(no output)";
|
|
1714
|
+
const sections = parseSections(trimmed);
|
|
1715
|
+
const findingTexts = collectBullets(sections, [/finding/, /issue/, /problem/, /vulnerab/]);
|
|
1716
|
+
const recommendationTexts = collectBullets(sections, [/recommend/, /suggestion/, /next step/, /action/]);
|
|
1717
|
+
const evidenceTexts = collectBullets(sections, [/evidence/, /proof/, /log/]);
|
|
1718
|
+
const findings = findingTexts.map((text, index) => ({
|
|
1719
|
+
id: `${meta.taskId}-f-${index + 1}`,
|
|
1720
|
+
title: truncate4(text, 120),
|
|
1721
|
+
detail: text,
|
|
1722
|
+
severity: guessSeverity(text)
|
|
1723
|
+
}));
|
|
1724
|
+
return {
|
|
1725
|
+
id: `${meta.taskId}-report`,
|
|
1726
|
+
taskId: meta.taskId,
|
|
1727
|
+
agent: meta.agent,
|
|
1728
|
+
...meta.model !== void 0 ? { model: meta.model } : {},
|
|
1729
|
+
summary,
|
|
1730
|
+
findings,
|
|
1731
|
+
recommendations: recommendationTexts.map((t) => truncate4(t, 200)),
|
|
1732
|
+
metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
|
|
1733
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
function truncate4(text, max) {
|
|
1572
1737
|
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1573
1738
|
}
|
|
1574
1739
|
|
|
1740
|
+
// src/reports/service.ts
|
|
1741
|
+
var ReportService = class {
|
|
1742
|
+
constructor(store, log = logger) {
|
|
1743
|
+
this.store = store;
|
|
1744
|
+
this.log = log.child({ component: "reports" });
|
|
1745
|
+
}
|
|
1746
|
+
store;
|
|
1747
|
+
log;
|
|
1748
|
+
get storeRef() {
|
|
1749
|
+
return this.store;
|
|
1750
|
+
}
|
|
1751
|
+
/** Normalizes finished tasks into reports (§17 — normalizer). */
|
|
1752
|
+
async collectFromTasks(tasks) {
|
|
1753
|
+
const reports = [];
|
|
1754
|
+
for (const task of tasks) {
|
|
1755
|
+
const structured = task.result?.reports ?? [];
|
|
1756
|
+
if (structured.length > 0) {
|
|
1757
|
+
reports.push(...structured);
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
const text = task.result?.rawOutput ?? task.result?.summary ?? "";
|
|
1761
|
+
if (text.trim().length === 0) continue;
|
|
1762
|
+
reports.push(
|
|
1763
|
+
extractReportFromText(text, {
|
|
1764
|
+
taskId: task.id,
|
|
1765
|
+
agent: task.agent ?? "unknown"
|
|
1766
|
+
})
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
return reports;
|
|
1770
|
+
}
|
|
1771
|
+
async saveReports(reports) {
|
|
1772
|
+
if (reports.length === 0) return;
|
|
1773
|
+
await this.store.saveReports(reports);
|
|
1774
|
+
this.log.info(`Stored ${reports.length} report(s).`);
|
|
1775
|
+
}
|
|
1776
|
+
async generateCombined(reports, meta = {}) {
|
|
1777
|
+
if (reports.length === 0) return void 0;
|
|
1778
|
+
const options = {
|
|
1779
|
+
...meta.objective !== void 0 ? { objective: meta.objective } : {},
|
|
1780
|
+
...meta.pipelineRunId !== void 0 ? { pipelineRunId: meta.pipelineRunId } : {},
|
|
1781
|
+
...meta.taskIds !== void 0 ? { taskIds: meta.taskIds } : {}
|
|
1782
|
+
};
|
|
1783
|
+
const combined = aggregateReports(reports, options);
|
|
1784
|
+
await this.store.saveCombined(combined);
|
|
1785
|
+
this.log.info(`Generated combined report ${combined.id}.`);
|
|
1786
|
+
return combined;
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1790
|
+
// src/reports/store.ts
|
|
1791
|
+
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
1792
|
+
import { dirname as dirname2 } from "path";
|
|
1793
|
+
var FileReportStore = class {
|
|
1794
|
+
filePath;
|
|
1795
|
+
queue = Promise.resolve();
|
|
1796
|
+
constructor(directory = `${process.cwd()}/.omninode`) {
|
|
1797
|
+
this.filePath = `${directory}/reports.json`;
|
|
1798
|
+
}
|
|
1799
|
+
async saveReports(reports) {
|
|
1800
|
+
return this.synchronized(async () => {
|
|
1801
|
+
const entries = await this.load();
|
|
1802
|
+
for (const report of reports) {
|
|
1803
|
+
const index = entries.findIndex(
|
|
1804
|
+
(e) => e.kind === "report" && e.report.id === report.id
|
|
1805
|
+
);
|
|
1806
|
+
const entry2 = { kind: "report", report };
|
|
1807
|
+
if (index >= 0) entries[index] = entry2;
|
|
1808
|
+
else entries.push(entry2);
|
|
1809
|
+
}
|
|
1810
|
+
await this.write(entries);
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
async listReports(filter = {}) {
|
|
1814
|
+
return this.synchronized(async () => {
|
|
1815
|
+
const entries = await this.load();
|
|
1816
|
+
return entries.filter((e) => e.kind === "report").map((e) => e.report).filter((report) => filter.taskId ? report.taskId === filter.taskId : true).filter((report) => filter.agent ? report.agent === filter.agent : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
async saveCombined(combined) {
|
|
1820
|
+
return this.synchronized(async () => {
|
|
1821
|
+
const entries = await this.load();
|
|
1822
|
+
const index = entries.findIndex(
|
|
1823
|
+
(e) => e.kind === "combined" && e.combined.id === combined.id
|
|
1824
|
+
);
|
|
1825
|
+
const entry2 = { kind: "combined", combined };
|
|
1826
|
+
if (index >= 0) entries[index] = entry2;
|
|
1827
|
+
else entries.push(entry2);
|
|
1828
|
+
await this.write(entries);
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
async getCombined(id) {
|
|
1832
|
+
return this.synchronized(async () => {
|
|
1833
|
+
const entries = await this.load();
|
|
1834
|
+
const found = entries.find((e) => e.kind === "combined" && e.combined.id === id);
|
|
1835
|
+
return found?.kind === "combined" ? found.combined : void 0;
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
async listCombined(filter = {}) {
|
|
1839
|
+
return this.synchronized(async () => {
|
|
1840
|
+
const entries = await this.load();
|
|
1841
|
+
return entries.filter((e) => e.kind === "combined").map((e) => e.combined).filter(
|
|
1842
|
+
(combined) => filter.pipelineRunId ? combined.pipelineRunId === filter.pipelineRunId : true
|
|
1843
|
+
).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
synchronized(operation) {
|
|
1847
|
+
const result = this.queue.then(operation, operation);
|
|
1848
|
+
this.queue = result.then(
|
|
1849
|
+
() => void 0,
|
|
1850
|
+
() => void 0
|
|
1851
|
+
);
|
|
1852
|
+
return result;
|
|
1853
|
+
}
|
|
1854
|
+
async load() {
|
|
1855
|
+
try {
|
|
1856
|
+
const raw = await readFile2(this.filePath, "utf8");
|
|
1857
|
+
const parsed = JSON.parse(raw);
|
|
1858
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1859
|
+
} catch {
|
|
1860
|
+
return [];
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async write(entries) {
|
|
1864
|
+
const tmp = `${this.filePath}.tmp`;
|
|
1865
|
+
await mkdir2(dirname2(this.filePath), { recursive: true });
|
|
1866
|
+
await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
|
|
1867
|
+
`, "utf8");
|
|
1868
|
+
await rename2(tmp, this.filePath);
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
|
|
1575
1872
|
// src/cli/commands/reports.ts
|
|
1873
|
+
function service() {
|
|
1874
|
+
return new ReportService(new FileReportStore());
|
|
1875
|
+
}
|
|
1576
1876
|
function registerReportCommands(program) {
|
|
1577
|
-
const report = program.command("report").description("Inspect AI reports (
|
|
1578
|
-
report.command("list").description("List
|
|
1579
|
-
|
|
1877
|
+
const report = program.command("report").description("Inspect AI reports collected from tasks and pipelines (\xA716\u2013\xA717).");
|
|
1878
|
+
report.command("list").description("List stored reports.").option("--task <taskId>", "Only reports for this task.").option("--agent <name>", "Only reports from this agent.").action(async (options) => {
|
|
1879
|
+
const svc = service();
|
|
1880
|
+
const reports = await svc.storeRef.listReports({
|
|
1881
|
+
...options.task !== void 0 ? { taskId: options.task } : {},
|
|
1882
|
+
...options.agent !== void 0 ? { agent: options.agent } : {}
|
|
1883
|
+
});
|
|
1884
|
+
if (reports.length === 0) {
|
|
1885
|
+
console.log("No reports stored yet. Run a pipeline to collect some.");
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
for (const r of reports) {
|
|
1889
|
+
console.log(`${r.id} ${r.agent.padEnd(10)} ${truncate(r.summary, 60)}`);
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
report.command("combined").description("List combined reports (aggregated intelligence, \xA717).").option("--run <pipelineRunId>", "Only combined reports for this pipeline run.").action(async (options) => {
|
|
1893
|
+
const svc = service();
|
|
1894
|
+
const combined = await svc.storeRef.listCombined({
|
|
1895
|
+
...options.run !== void 0 ? { pipelineRunId: options.run } : {}
|
|
1896
|
+
});
|
|
1897
|
+
if (combined.length === 0) {
|
|
1898
|
+
console.log("No combined reports stored yet. Run a pipeline to generate one.");
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
for (const c of combined) {
|
|
1902
|
+
console.log(
|
|
1903
|
+
`${c.id} ${c.findings.length} finding(s) from ${c.sources.length} source(s)${c.pipelineRunId ? ` run=${c.pipelineRunId}` : ""}`
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1907
|
+
report.command("show <id>").description("Show a stored report or combined report in full.").action(async (id) => {
|
|
1908
|
+
const svc = service();
|
|
1909
|
+
const reports = await svc.storeRef.listReports();
|
|
1910
|
+
const raw = reports.find((r) => r.id === id);
|
|
1911
|
+
if (raw) {
|
|
1912
|
+
console.log(`Report ${raw.id}`);
|
|
1913
|
+
console.log(` agent: ${raw.agent}${raw.model ? ` (${raw.model})` : ""}`);
|
|
1914
|
+
console.log(` task: ${raw.taskId}`);
|
|
1915
|
+
console.log(` summary: ${raw.summary}`);
|
|
1916
|
+
if (raw.findings.length > 0) {
|
|
1917
|
+
console.log(" findings:");
|
|
1918
|
+
for (const finding of raw.findings) {
|
|
1919
|
+
console.log(` - [${finding.severity ?? "info"}] ${finding.title}`);
|
|
1920
|
+
for (const evidence of finding.evidence ?? []) {
|
|
1921
|
+
console.log(` evidence: ${evidence}`);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (raw.recommendations.length > 0) {
|
|
1926
|
+
console.log(" recommendations:");
|
|
1927
|
+
for (const recommendation of raw.recommendations) {
|
|
1928
|
+
console.log(` - ${recommendation}`);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
const combined = await svc.storeRef.getCombined(id);
|
|
1934
|
+
if (combined) {
|
|
1935
|
+
console.log(`Combined report ${combined.id}`);
|
|
1936
|
+
console.log(` sources: ${combined.sources.join(", ")}`);
|
|
1937
|
+
console.log(` reports: ${combined.reportIds.length}`);
|
|
1938
|
+
if (combined.pipelineRunId) console.log(` run: ${combined.pipelineRunId}`);
|
|
1939
|
+
console.log(` summary: ${combined.summary}`);
|
|
1940
|
+
if (combined.findings.length > 0) {
|
|
1941
|
+
console.log(" findings (grouped, duplicates merged):");
|
|
1942
|
+
for (const finding of combined.findings) {
|
|
1943
|
+
console.log(
|
|
1944
|
+
` - [${finding.severity ?? "info"}] ${finding.title} (x${finding.occurrences}, sources: ${finding.sources.map((s) => s.agent).join(", ")})`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
if (combined.recommendations.length > 0) {
|
|
1949
|
+
console.log(" recommendations:");
|
|
1950
|
+
for (const recommendation of combined.recommendations) {
|
|
1951
|
+
console.log(` - ${recommendation.text} (x${recommendation.occurrences})`);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
throw new OmniNodeError("REPORT_NOT_FOUND", `Report "${id}" was not found in the report store.`);
|
|
1580
1957
|
});
|
|
1581
1958
|
}
|
|
1582
1959
|
|
|
1583
1960
|
// src/pipelines/engine.ts
|
|
1584
|
-
import { randomBytes as
|
|
1961
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1585
1962
|
var PipelineEngine = class {
|
|
1586
1963
|
constructor(options) {
|
|
1587
1964
|
this.options = options;
|
|
@@ -1694,7 +2071,7 @@ var PipelineEngine = class {
|
|
|
1694
2071
|
}
|
|
1695
2072
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1696
2073
|
const run2 = {
|
|
1697
|
-
id: `run-${Date.now().toString(36)}-${
|
|
2074
|
+
id: `run-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
|
|
1698
2075
|
pipelineId: def.id,
|
|
1699
2076
|
status: "running",
|
|
1700
2077
|
startedAt: now,
|
|
@@ -1751,11 +2128,28 @@ ${contribution}`);
|
|
|
1751
2128
|
}
|
|
1752
2129
|
const failed = [...outcomes.values()].some((o) => o.status === "failed");
|
|
1753
2130
|
const finalStatus = failed ? "failed" : "completed";
|
|
2131
|
+
let combinedReportId;
|
|
2132
|
+
if (this.options.reports) {
|
|
2133
|
+
const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
|
|
2134
|
+
const tasks = (await Promise.all(taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
|
|
2135
|
+
const collected = await this.options.reports.collectFromTasks(tasks);
|
|
2136
|
+
await this.options.reports.saveReports(collected);
|
|
2137
|
+
const combined = await this.options.reports.generateCombined(collected, {
|
|
2138
|
+
objective,
|
|
2139
|
+
pipelineRunId: run2.id,
|
|
2140
|
+
taskIds
|
|
2141
|
+
});
|
|
2142
|
+
combinedReportId = combined?.id;
|
|
2143
|
+
if (collected.length > 0) {
|
|
2144
|
+
run2.reports = collected;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
1754
2147
|
const finished = {
|
|
1755
2148
|
...run2,
|
|
1756
2149
|
status: finalStatus,
|
|
1757
2150
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1758
|
-
...
|
|
2151
|
+
...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
|
|
2152
|
+
...combinedReportId !== void 0 ? { combinedReportId } : {}
|
|
1759
2153
|
};
|
|
1760
2154
|
await this.options.store.save(finished);
|
|
1761
2155
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
@@ -1935,8 +2329,8 @@ function createDefaultChatFn(providers) {
|
|
|
1935
2329
|
}
|
|
1936
2330
|
|
|
1937
2331
|
// src/pipelines/store.ts
|
|
1938
|
-
import { mkdir as
|
|
1939
|
-
import { dirname as
|
|
2332
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
|
|
2333
|
+
import { dirname as dirname3 } from "path";
|
|
1940
2334
|
var FilePipelineRunStore = class {
|
|
1941
2335
|
filePath;
|
|
1942
2336
|
queue = Promise.resolve();
|
|
@@ -1974,7 +2368,7 @@ var FilePipelineRunStore = class {
|
|
|
1974
2368
|
}
|
|
1975
2369
|
async load() {
|
|
1976
2370
|
try {
|
|
1977
|
-
const raw = await
|
|
2371
|
+
const raw = await readFile3(this.filePath, "utf8");
|
|
1978
2372
|
const parsed = JSON.parse(raw);
|
|
1979
2373
|
return Array.isArray(parsed) ? parsed : [];
|
|
1980
2374
|
} catch {
|
|
@@ -1983,10 +2377,10 @@ var FilePipelineRunStore = class {
|
|
|
1983
2377
|
}
|
|
1984
2378
|
async write(runs) {
|
|
1985
2379
|
const tmp = `${this.filePath}.tmp`;
|
|
1986
|
-
await
|
|
1987
|
-
await
|
|
2380
|
+
await mkdir3(dirname3(this.filePath), { recursive: true });
|
|
2381
|
+
await writeFile3(tmp, `${JSON.stringify(runs, null, 2)}
|
|
1988
2382
|
`, "utf8");
|
|
1989
|
-
await
|
|
2383
|
+
await rename3(tmp, this.filePath);
|
|
1990
2384
|
}
|
|
1991
2385
|
};
|
|
1992
2386
|
|
|
@@ -2007,6 +2401,7 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
2007
2401
|
providers: config.project.providers,
|
|
2008
2402
|
store: new FilePipelineRunStore(),
|
|
2009
2403
|
chat: createDefaultChatFn(config.project.providers),
|
|
2404
|
+
reports: new ReportService(new FileReportStore(), log),
|
|
2010
2405
|
log
|
|
2011
2406
|
});
|
|
2012
2407
|
}
|
|
@@ -2045,6 +2440,9 @@ function registerPipelineCommands(program) {
|
|
|
2045
2440
|
if (finished.reports && finished.reports.length > 0) {
|
|
2046
2441
|
console.log(` reports: ${finished.reports.length} structured report(s)`);
|
|
2047
2442
|
}
|
|
2443
|
+
if (finished.combinedReportId) {
|
|
2444
|
+
console.log(` combined report: ${finished.combinedReportId}`);
|
|
2445
|
+
}
|
|
2048
2446
|
console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
|
|
2049
2447
|
if (finished.status !== "completed") process.exitCode = 1;
|
|
2050
2448
|
});
|