@hilbras/omninode 0.6.0 → 0.8.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/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.6.0";
9
+ var OMNINODE_VERSION = "0.8.0";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -49,6 +49,11 @@ var ProtocolError = class extends OmniNodeError {
49
49
  super("PROTOCOL_VIOLATION", message, options);
50
50
  }
51
51
  };
52
+ var MemoryError = class extends OmniNodeError {
53
+ constructor(message, options = {}) {
54
+ super("MEMORY_UNAVAILABLE", message, options);
55
+ }
56
+ };
52
57
  function isOmniNodeError(value) {
53
58
  return value instanceof OmniNodeError;
54
59
  }
@@ -112,7 +117,11 @@ var projectConfigSchema = z.object({
112
117
  agents: z.array(agentConfigSchema).default([]),
113
118
  roles: z.array(roleConfigSchema).default([]),
114
119
  pipelines: z.array(pipelineConfigSchema).default([]),
115
- memory: z.object({ provider: z.string().min(1) }).strict().optional()
120
+ memory: z.object({
121
+ provider: z.string().min(1).default("local"),
122
+ base_url: z.string().url().optional(),
123
+ api_key_env_var: z.string().min(1).optional()
124
+ }).strict().optional()
116
125
  }).strict();
117
126
  var appConfigSchema = z.object({
118
127
  project: projectConfigSchema,
@@ -182,7 +191,13 @@ function toAppConfig(yaml) {
182
191
  agents: yaml.project.agents.map(toAgentConfig),
183
192
  roles: yaml.project.roles.map(toRoleDefinition),
184
193
  pipelines: yaml.project.pipelines.map(toPipelineDefinition),
185
- ...yaml.project.memory !== void 0 ? { memory: yaml.project.memory } : {}
194
+ ...yaml.project.memory !== void 0 ? {
195
+ memory: {
196
+ provider: yaml.project.memory.provider,
197
+ baseUrl: yaml.project.memory.base_url,
198
+ apiKeyEnvVar: yaml.project.memory.api_key_env_var
199
+ }
200
+ } : {}
186
201
  },
187
202
  ...yaml.logging !== void 0 ? { logging: yaml.logging } : {}
188
203
  };
@@ -675,6 +690,9 @@ function notImplemented(command, when) {
675
690
  `${command} is planned for ${when}. Edit omninode.yaml directly for now.`
676
691
  );
677
692
  }
693
+ function truncate(text, max) {
694
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
695
+ }
678
696
  function appendToConfigList(listKey, item) {
679
697
  const configPath = findConfigFile();
680
698
  if (!configPath) {
@@ -1386,13 +1404,24 @@ var TaskEngine = class {
1386
1404
  const role = task.role ? this.options.roles.get(task.role) : void 0;
1387
1405
  const running = await this.transition(task, "running");
1388
1406
  this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1407
+ let background = running.context?.background;
1408
+ if (this.options.memory) {
1409
+ const memoryContext = await this.options.memory.gatherContext({
1410
+ objective: running.objective,
1411
+ ...running.role ? { role: running.role } : {},
1412
+ ...running.project ? { project: running.project } : {}
1413
+ });
1414
+ if (memoryContext.length > 0) {
1415
+ background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
1416
+ }
1417
+ }
1389
1418
  let output;
1390
1419
  try {
1391
1420
  output = await agent.run({
1392
1421
  taskId: running.id,
1393
1422
  objective: running.objective,
1394
1423
  ...role ? { role } : {},
1395
- ...running.context?.background ? { context: running.context.background } : {}
1424
+ ...background !== void 0 ? { context: background } : {}
1396
1425
  });
1397
1426
  } catch (error) {
1398
1427
  const message = error instanceof Error ? error.message : String(error);
@@ -1404,12 +1433,18 @@ var TaskEngine = class {
1404
1433
  const result = {
1405
1434
  ...output.summary !== void 0 ? { summary: output.summary } : {},
1406
1435
  ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1436
+ // Keep the verbatim output (bounded) for report extraction and audit.
1437
+ ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1407
1438
  ...output.error !== void 0 ? { error: output.error } : {},
1408
1439
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1409
1440
  };
1410
1441
  const finalStatus = output.status === "completed" ? "completed" : "failed";
1442
+ const finished = await this.transition(running, finalStatus, { result });
1411
1443
  this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
1412
- return this.transition(running, finalStatus, { result });
1444
+ if (this.options.memory) {
1445
+ await this.options.memory.recordTaskOutcome(finished);
1446
+ }
1447
+ return finished;
1413
1448
  }
1414
1449
  async cancel(id) {
1415
1450
  const task = await this.mustGet(id);
@@ -1454,6 +1489,297 @@ function generateTaskId() {
1454
1489
  return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1455
1490
  }
1456
1491
 
1492
+ // src/memory/local.ts
1493
+ import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1494
+ import { dirname as dirname2 } from "path";
1495
+ var STOPWORDS = /* @__PURE__ */ new Set([
1496
+ "the",
1497
+ "and",
1498
+ "for",
1499
+ "with",
1500
+ "this",
1501
+ "that",
1502
+ "from",
1503
+ "into",
1504
+ "was",
1505
+ "were",
1506
+ "have",
1507
+ "has",
1508
+ "had",
1509
+ "are",
1510
+ "but",
1511
+ "not",
1512
+ "you",
1513
+ "his",
1514
+ "her",
1515
+ "its",
1516
+ "their",
1517
+ "them",
1518
+ "then",
1519
+ "than",
1520
+ "when",
1521
+ "what",
1522
+ "which",
1523
+ "will",
1524
+ "would",
1525
+ "been",
1526
+ "were",
1527
+ "also",
1528
+ "because",
1529
+ "about",
1530
+ "after",
1531
+ "before"
1532
+ ]);
1533
+ function keywords(text) {
1534
+ return new Set(
1535
+ (text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4 && !STOPWORDS.has(t))
1536
+ );
1537
+ }
1538
+ function relevanceScore(queryKeywords, entry2) {
1539
+ if (queryKeywords.size === 0) return 0;
1540
+ const entryKeywords = keywords(`${entry2.content} ${entry2.key}`);
1541
+ let overlap = 0;
1542
+ for (const keyword of queryKeywords) {
1543
+ if (entryKeywords.has(keyword)) overlap += 1;
1544
+ }
1545
+ let tagMatches = 0;
1546
+ for (const tag of entry2.tags ?? []) {
1547
+ if (queryKeywords.has(tag.toLowerCase())) tagMatches += 1;
1548
+ }
1549
+ return overlap * 2 + tagMatches * 1.5;
1550
+ }
1551
+ var LocalMemoryProvider = class {
1552
+ name = "local";
1553
+ filePath;
1554
+ queue = Promise.resolve();
1555
+ constructor(directory = `${process.cwd()}/.omninode`) {
1556
+ this.filePath = `${directory}/memory.json`;
1557
+ }
1558
+ async query(query) {
1559
+ return this.synchronized(async () => {
1560
+ const entries = await this.load();
1561
+ const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter(
1562
+ (entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
1563
+ );
1564
+ if (!query.text || query.text.trim().length === 0) {
1565
+ return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
1566
+ }
1567
+ const queryKeywords = keywords(query.text);
1568
+ return filtered.map((entry2) => ({ entry: entry2, score: relevanceScore(queryKeywords, entry2) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry: entry2 }) => entry2);
1569
+ });
1570
+ }
1571
+ async write(entry2) {
1572
+ return this.synchronized(async () => {
1573
+ const entries = await this.load();
1574
+ const index = entries.findIndex((e) => e.key === entry2.key);
1575
+ const stamped = {
1576
+ ...entry2,
1577
+ createdAt: entry2.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1578
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1579
+ };
1580
+ if (index >= 0) entries[index] = stamped;
1581
+ else entries.push(stamped);
1582
+ await this.saveAll(entries);
1583
+ });
1584
+ }
1585
+ async count() {
1586
+ return (await this.load()).length;
1587
+ }
1588
+ synchronized(operation) {
1589
+ const result = this.queue.then(operation, operation);
1590
+ this.queue = result.then(
1591
+ () => void 0,
1592
+ () => void 0
1593
+ );
1594
+ return result;
1595
+ }
1596
+ async load() {
1597
+ try {
1598
+ const raw = await readFile2(this.filePath, "utf8");
1599
+ const parsed = JSON.parse(raw);
1600
+ return Array.isArray(parsed) ? parsed : [];
1601
+ } catch {
1602
+ return [];
1603
+ }
1604
+ }
1605
+ async saveAll(entries) {
1606
+ const tmp = `${this.filePath}.tmp`;
1607
+ await mkdir2(dirname2(this.filePath), { recursive: true });
1608
+ await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
1609
+ `, "utf8");
1610
+ await rename2(tmp, this.filePath);
1611
+ }
1612
+ };
1613
+
1614
+ // src/memory/remembera.ts
1615
+ var RememberaMemoryProvider = class {
1616
+ name = "remembera";
1617
+ baseUrl;
1618
+ apiKeyEnvVar;
1619
+ env;
1620
+ log;
1621
+ constructor(config, log = logger) {
1622
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
1623
+ this.apiKeyEnvVar = config.apiKeyEnvVar;
1624
+ this.env = config.env ?? process.env;
1625
+ this.log = log.child({ component: "remembera" });
1626
+ }
1627
+ headers() {
1628
+ const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
1629
+ return key ? { authorization: `Bearer ${key}` } : {};
1630
+ }
1631
+ async query(query) {
1632
+ const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
1633
+ if (status !== 200) {
1634
+ throw new MemoryError(`Remembera query returned HTTP ${status}.`);
1635
+ }
1636
+ const entries = body.entries;
1637
+ if (!Array.isArray(entries)) {
1638
+ throw new MemoryError("Remembera query response had no entries array.");
1639
+ }
1640
+ return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
1641
+ }
1642
+ async write(entry2) {
1643
+ const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
1644
+ if (status !== 200 && status !== 201) {
1645
+ throw new MemoryError(`Remembera write returned HTTP ${status}.`);
1646
+ }
1647
+ }
1648
+ async request(url, body) {
1649
+ const headers = this.headers();
1650
+ try {
1651
+ return await requestJson(url, {
1652
+ method: "POST",
1653
+ headers,
1654
+ body
1655
+ });
1656
+ } catch (error) {
1657
+ const message = error instanceof Error ? error.message : String(error);
1658
+ throw new MemoryError(`Remembera request to ${url} failed: ${message}`, { cause: error });
1659
+ }
1660
+ }
1661
+ toEntry(raw) {
1662
+ if (raw === null || typeof raw !== "object") {
1663
+ this.log.warn("Skipping malformed Remembera memory entry.");
1664
+ return void 0;
1665
+ }
1666
+ const entry2 = raw;
1667
+ if (typeof entry2.key !== "string" || typeof entry2.content !== "string") {
1668
+ this.log.warn("Skipping Remembera memory entry without key/content.");
1669
+ return void 0;
1670
+ }
1671
+ const scope = entry2.scope;
1672
+ return {
1673
+ key: entry2.key,
1674
+ content: entry2.content,
1675
+ scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
1676
+ ...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
1677
+ ...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
1678
+ ...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
1679
+ };
1680
+ }
1681
+ };
1682
+
1683
+ // src/memory/factory.ts
1684
+ function createMemoryProvider(config) {
1685
+ if (!config || config.provider === "local") {
1686
+ return new LocalMemoryProvider();
1687
+ }
1688
+ if (config.provider === "remembera") {
1689
+ if (!config.baseUrl) {
1690
+ throw new ConfigError(
1691
+ "CONFIG_INVALID",
1692
+ `Memory provider "remembera" needs a base_url in omninode.yaml.`
1693
+ );
1694
+ }
1695
+ return new RememberaMemoryProvider({
1696
+ baseUrl: config.baseUrl,
1697
+ apiKeyEnvVar: config.apiKeyEnvVar
1698
+ });
1699
+ }
1700
+ throw new ConfigError(
1701
+ "CONFIG_INVALID",
1702
+ `Unknown memory provider "${config.provider}". Available: local, remembera.`
1703
+ );
1704
+ }
1705
+
1706
+ // src/memory/service.ts
1707
+ var MAX_CONTEXT_CHARS = 4e3;
1708
+ var MemoryService = class {
1709
+ constructor(provider, log = logger) {
1710
+ this.provider = provider;
1711
+ this.log = log.child({ component: "memory", provider: provider.name });
1712
+ }
1713
+ provider;
1714
+ log;
1715
+ get providerRef() {
1716
+ return this.provider;
1717
+ }
1718
+ get providerName() {
1719
+ return this.provider.name;
1720
+ }
1721
+ /** Relevant context for a task objective; empty string when nothing matches. */
1722
+ async gatherContext(query) {
1723
+ try {
1724
+ const entries = await this.provider.query({
1725
+ text: query.objective,
1726
+ ...query.role ? { tags: [query.role] } : {},
1727
+ limit: 8
1728
+ });
1729
+ if (entries.length === 0) return "";
1730
+ const byScope = /* @__PURE__ */ new Map();
1731
+ for (const entry2 of entries) {
1732
+ byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
1733
+ }
1734
+ const parts = [];
1735
+ for (const scope of ["project", "task", "role", "knowledge"]) {
1736
+ const scopeEntries = byScope.get(scope) ?? [];
1737
+ if (scopeEntries.length === 0) continue;
1738
+ const lines = scopeEntries.map((entry2) => `- ${entry2.content}`).join("\n");
1739
+ parts.push(`### Relevant memory (${scope})
1740
+ ${lines}`);
1741
+ }
1742
+ const context = parts.join("\n\n");
1743
+ return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
1744
+ } catch (error) {
1745
+ this.log.warn(
1746
+ `Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
1747
+ );
1748
+ return "";
1749
+ }
1750
+ }
1751
+ /** Records a finished task's outcome — best-effort. */
1752
+ async recordTaskOutcome(task) {
1753
+ const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
1754
+ try {
1755
+ await this.provider.write({
1756
+ key: `task:${task.id}`,
1757
+ scope: "task",
1758
+ content: `Objective: ${task.objective}
1759
+ Status: ${task.status}
1760
+ Outcome: ${outcome}`,
1761
+ tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
1762
+ createdAt: task.createdAt
1763
+ });
1764
+ const significant = (task.result?.reports ?? []).flatMap(
1765
+ (report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
1766
+ );
1767
+ for (const { report, finding } of significant) {
1768
+ await this.provider.write({
1769
+ key: `finding:${report.id}:${finding.id}`,
1770
+ scope: "project",
1771
+ content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
1772
+ tags: ["known-problem", report.agent]
1773
+ });
1774
+ }
1775
+ } catch (error) {
1776
+ this.log.warn(
1777
+ `Memory write failed (continuing): ${error instanceof Error ? error.message : String(error)}`
1778
+ );
1779
+ }
1780
+ }
1781
+ };
1782
+
1457
1783
  // src/roles/index.ts
1458
1784
  var RoleRegistry = class {
1459
1785
  roles = /* @__PURE__ */ new Map();
@@ -1478,10 +1804,13 @@ function buildRoleRegistry(config) {
1478
1804
 
1479
1805
  // src/tasks/engine-factory.ts
1480
1806
  function createTaskEngine(config, log = logger, directory) {
1807
+ const agents = buildAgentRegistry(config, log);
1808
+ const roles = buildRoleRegistry(config);
1481
1809
  return new TaskEngine({
1482
- agents: buildAgentRegistry(config, log),
1483
- roles: buildRoleRegistry(config),
1810
+ agents,
1811
+ roles,
1484
1812
  store: new FileTaskStore(directory),
1813
+ ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
1485
1814
  log
1486
1815
  });
1487
1816
  }
@@ -1538,7 +1867,7 @@ function registerTaskCommands(program) {
1538
1867
  return;
1539
1868
  }
1540
1869
  for (const t of tasks) {
1541
- console.log(`${t.id} ${t.status.padEnd(10)} ${truncate(t.objective, 60)}`);
1870
+ console.log(`${t.id} ${t.status.padEnd(10)} ${truncate2(t.objective, 60)}`);
1542
1871
  }
1543
1872
  });
1544
1873
  task.command("cancel <task-id>").description("Cancel a task that has not finished yet.").action(async (taskId) => {
@@ -1568,20 +1897,392 @@ function printOutcome(task, options = {}) {
1568
1897
  if (result.error) console.log(` error: ${result.error}`);
1569
1898
  if (result.finishedAt) console.log(` ended: ${result.finishedAt}`);
1570
1899
  }
1571
- function truncate(text, max) {
1900
+ function truncate2(text, max) {
1572
1901
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1573
1902
  }
1574
1903
 
1904
+ // src/reports/aggregate.ts
1905
+ import { randomBytes as randomBytes2 } from "crypto";
1906
+ var SEVERITY_RANK = {
1907
+ info: 0,
1908
+ low: 1,
1909
+ medium: 2,
1910
+ high: 3,
1911
+ critical: 4
1912
+ };
1913
+ function aggregateReports(reports, options = {}) {
1914
+ const threshold = options.similarityThreshold ?? 0.7;
1915
+ const findingGroups = [];
1916
+ const recommendationGroups = [];
1917
+ for (const report of reports) {
1918
+ for (const finding of report.findings) {
1919
+ mergeFinding(findingGroups, finding, report, threshold);
1920
+ }
1921
+ for (const text of report.recommendations) {
1922
+ mergeRecommendation(recommendationGroups, text, report, threshold);
1923
+ }
1924
+ }
1925
+ const sources = [...new Set(reports.map((r) => r.agent))].sort();
1926
+ const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
1927
+ 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).`;
1928
+ return {
1929
+ id: `combined-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
1930
+ title: options.objective !== void 0 ? truncate3(options.objective, 120) : "Combined report",
1931
+ ...options.objective !== void 0 ? { objective: options.objective } : {},
1932
+ ...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
1933
+ taskIds: options.taskIds ?? [],
1934
+ summary,
1935
+ findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
1936
+ recommendations: recommendationGroups,
1937
+ sources,
1938
+ reportIds: reports.map((r) => r.id),
1939
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1940
+ };
1941
+ }
1942
+ function rank(finding) {
1943
+ return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
1944
+ }
1945
+ function mergeFinding(groups, finding, report, threshold) {
1946
+ const source = { agent: report.agent, reportId: report.id };
1947
+ const group = groups.find((g) => similar(g.title, finding.title, threshold));
1948
+ if (group) {
1949
+ group.occurrences += 1;
1950
+ group.details.push(finding.detail);
1951
+ group.sources.push(source);
1952
+ group.evidence.push(...finding.evidence ?? []);
1953
+ if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
1954
+ group.severity = finding.severity;
1955
+ }
1956
+ return;
1957
+ }
1958
+ groups.push({
1959
+ title: finding.title,
1960
+ details: [finding.detail],
1961
+ sources: [source],
1962
+ occurrences: 1,
1963
+ ...finding.severity ? { severity: finding.severity } : {},
1964
+ evidence: [...finding.evidence ?? []]
1965
+ });
1966
+ }
1967
+ function mergeRecommendation(groups, text, report, threshold) {
1968
+ const source = { agent: report.agent, reportId: report.id };
1969
+ const group = groups.find((g) => similar(g.text, text, threshold));
1970
+ if (group) {
1971
+ group.occurrences += 1;
1972
+ group.sources.push(source);
1973
+ return;
1974
+ }
1975
+ groups.push({ text, sources: [source], occurrences: 1 });
1976
+ }
1977
+ function similar(a, b, threshold) {
1978
+ const left = tokens(a);
1979
+ const right = tokens(b);
1980
+ if (left.size === 0 || right.size === 0) return false;
1981
+ let intersection = 0;
1982
+ for (const token of left) {
1983
+ if (right.has(token)) intersection += 1;
1984
+ }
1985
+ const union = left.size + right.size - intersection;
1986
+ return intersection / union >= threshold;
1987
+ }
1988
+ function tokens(text) {
1989
+ return new Set(
1990
+ text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
1991
+ );
1992
+ }
1993
+ function truncate3(text, max) {
1994
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1995
+ }
1996
+
1997
+ // src/reports/extract.ts
1998
+ var BULLET = /^(?:[-*•]|\d+[.)])\s+(.*)$/;
1999
+ function parseSections(raw) {
2000
+ const sections = [];
2001
+ let current = { heading: "", bullets: [] };
2002
+ for (const line of raw.split("\n")) {
2003
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line.trim());
2004
+ if (heading) {
2005
+ if (current.heading.length > 0 || current.bullets.length > 0) {
2006
+ sections.push(current);
2007
+ }
2008
+ current = { heading: heading[2]?.toLowerCase() ?? "", bullets: [] };
2009
+ continue;
2010
+ }
2011
+ const bullet = BULLET.exec(line.trim());
2012
+ if (bullet && current.heading.length > 0) {
2013
+ current.bullets.push(bullet[1]?.trim() ?? "");
2014
+ }
2015
+ }
2016
+ if (current.heading.length > 0 || current.bullets.length > 0) {
2017
+ sections.push(current);
2018
+ }
2019
+ return sections;
2020
+ }
2021
+ function collectBullets(sections, patterns) {
2022
+ const matches = sections.filter((section) => patterns.some((p) => p.test(section.heading)));
2023
+ return matches.flatMap((section) => section.bullets).filter((b) => b.length > 0);
2024
+ }
2025
+ function guessSeverity(text) {
2026
+ if (/critical|severe/i.test(text)) return "critical";
2027
+ if (/\bhigh\b|major/i.test(text)) return "high";
2028
+ if (/medium|moderate/i.test(text)) return "medium";
2029
+ if (/\blow\b|minor/i.test(text)) return "low";
2030
+ return void 0;
2031
+ }
2032
+ function extractReportFromText(raw, meta) {
2033
+ const trimmed = raw.trim();
2034
+ const nonEmpty = trimmed.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
2035
+ const proseLine = nonEmpty.find((line) => !line.startsWith("#") && !BULLET.test(line)) ?? nonEmpty[0] ?? "";
2036
+ const cleaned = proseLine.replace(/^#{1,6}\s+/, "").replace(BULLET, "$1");
2037
+ const summary = cleaned.length > 0 ? truncate4(cleaned, 200) : "(no output)";
2038
+ const sections = parseSections(trimmed);
2039
+ const findingTexts = collectBullets(sections, [/finding/, /issue/, /problem/, /vulnerab/]);
2040
+ const recommendationTexts = collectBullets(sections, [/recommend/, /suggestion/, /next step/, /action/]);
2041
+ const evidenceTexts = collectBullets(sections, [/evidence/, /proof/, /log/]);
2042
+ const findings = findingTexts.map((text, index) => ({
2043
+ id: `${meta.taskId}-f-${index + 1}`,
2044
+ title: truncate4(text, 120),
2045
+ detail: text,
2046
+ severity: guessSeverity(text)
2047
+ }));
2048
+ return {
2049
+ id: `${meta.taskId}-report`,
2050
+ taskId: meta.taskId,
2051
+ agent: meta.agent,
2052
+ ...meta.model !== void 0 ? { model: meta.model } : {},
2053
+ summary,
2054
+ findings,
2055
+ recommendations: recommendationTexts.map((t) => truncate4(t, 200)),
2056
+ metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
2057
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2058
+ };
2059
+ }
2060
+ function truncate4(text, max) {
2061
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
2062
+ }
2063
+
2064
+ // src/reports/service.ts
2065
+ var ReportService = class {
2066
+ constructor(store, log = logger) {
2067
+ this.store = store;
2068
+ this.log = log.child({ component: "reports" });
2069
+ }
2070
+ store;
2071
+ log;
2072
+ get storeRef() {
2073
+ return this.store;
2074
+ }
2075
+ /** Normalizes finished tasks into reports (§17 — normalizer). */
2076
+ async collectFromTasks(tasks) {
2077
+ const reports = [];
2078
+ for (const task of tasks) {
2079
+ const structured = task.result?.reports ?? [];
2080
+ if (structured.length > 0) {
2081
+ reports.push(...structured);
2082
+ continue;
2083
+ }
2084
+ const text = task.result?.rawOutput ?? task.result?.summary ?? "";
2085
+ if (text.trim().length === 0) continue;
2086
+ reports.push(
2087
+ extractReportFromText(text, {
2088
+ taskId: task.id,
2089
+ agent: task.agent ?? "unknown"
2090
+ })
2091
+ );
2092
+ }
2093
+ return reports;
2094
+ }
2095
+ async saveReports(reports) {
2096
+ if (reports.length === 0) return;
2097
+ await this.store.saveReports(reports);
2098
+ this.log.info(`Stored ${reports.length} report(s).`);
2099
+ }
2100
+ async generateCombined(reports, meta = {}) {
2101
+ if (reports.length === 0) return void 0;
2102
+ const options = {
2103
+ ...meta.objective !== void 0 ? { objective: meta.objective } : {},
2104
+ ...meta.pipelineRunId !== void 0 ? { pipelineRunId: meta.pipelineRunId } : {},
2105
+ ...meta.taskIds !== void 0 ? { taskIds: meta.taskIds } : {}
2106
+ };
2107
+ const combined = aggregateReports(reports, options);
2108
+ await this.store.saveCombined(combined);
2109
+ this.log.info(`Generated combined report ${combined.id}.`);
2110
+ return combined;
2111
+ }
2112
+ };
2113
+
2114
+ // src/reports/store.ts
2115
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
2116
+ import { dirname as dirname3 } from "path";
2117
+ var FileReportStore = class {
2118
+ filePath;
2119
+ queue = Promise.resolve();
2120
+ constructor(directory = `${process.cwd()}/.omninode`) {
2121
+ this.filePath = `${directory}/reports.json`;
2122
+ }
2123
+ async saveReports(reports) {
2124
+ return this.synchronized(async () => {
2125
+ const entries = await this.load();
2126
+ for (const report of reports) {
2127
+ const index = entries.findIndex(
2128
+ (e) => e.kind === "report" && e.report.id === report.id
2129
+ );
2130
+ const entry2 = { kind: "report", report };
2131
+ if (index >= 0) entries[index] = entry2;
2132
+ else entries.push(entry2);
2133
+ }
2134
+ await this.write(entries);
2135
+ });
2136
+ }
2137
+ async listReports(filter = {}) {
2138
+ return this.synchronized(async () => {
2139
+ const entries = await this.load();
2140
+ 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));
2141
+ });
2142
+ }
2143
+ async saveCombined(combined) {
2144
+ return this.synchronized(async () => {
2145
+ const entries = await this.load();
2146
+ const index = entries.findIndex(
2147
+ (e) => e.kind === "combined" && e.combined.id === combined.id
2148
+ );
2149
+ const entry2 = { kind: "combined", combined };
2150
+ if (index >= 0) entries[index] = entry2;
2151
+ else entries.push(entry2);
2152
+ await this.write(entries);
2153
+ });
2154
+ }
2155
+ async getCombined(id) {
2156
+ return this.synchronized(async () => {
2157
+ const entries = await this.load();
2158
+ const found = entries.find((e) => e.kind === "combined" && e.combined.id === id);
2159
+ return found?.kind === "combined" ? found.combined : void 0;
2160
+ });
2161
+ }
2162
+ async listCombined(filter = {}) {
2163
+ return this.synchronized(async () => {
2164
+ const entries = await this.load();
2165
+ return entries.filter((e) => e.kind === "combined").map((e) => e.combined).filter(
2166
+ (combined) => filter.pipelineRunId ? combined.pipelineRunId === filter.pipelineRunId : true
2167
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2168
+ });
2169
+ }
2170
+ synchronized(operation) {
2171
+ const result = this.queue.then(operation, operation);
2172
+ this.queue = result.then(
2173
+ () => void 0,
2174
+ () => void 0
2175
+ );
2176
+ return result;
2177
+ }
2178
+ async load() {
2179
+ try {
2180
+ const raw = await readFile3(this.filePath, "utf8");
2181
+ const parsed = JSON.parse(raw);
2182
+ return Array.isArray(parsed) ? parsed : [];
2183
+ } catch {
2184
+ return [];
2185
+ }
2186
+ }
2187
+ async write(entries) {
2188
+ const tmp = `${this.filePath}.tmp`;
2189
+ await mkdir3(dirname3(this.filePath), { recursive: true });
2190
+ await writeFile3(tmp, `${JSON.stringify(entries, null, 2)}
2191
+ `, "utf8");
2192
+ await rename3(tmp, this.filePath);
2193
+ }
2194
+ };
2195
+
1575
2196
  // src/cli/commands/reports.ts
2197
+ function service() {
2198
+ return new ReportService(new FileReportStore());
2199
+ }
1576
2200
  function registerReportCommands(program) {
1577
- const report = program.command("report").description("Inspect AI reports (Phase 6+).");
1578
- report.command("list").description("List collected reports (planned for Phase 6 \u2014 Multi-AI Report System).").action(() => {
1579
- throw notImplemented("omninode report list", "Phase 6 (Multi-AI Report System)");
2201
+ const report = program.command("report").description("Inspect AI reports collected from tasks and pipelines (\xA716\u2013\xA717).");
2202
+ 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) => {
2203
+ const svc = service();
2204
+ const reports = await svc.storeRef.listReports({
2205
+ ...options.task !== void 0 ? { taskId: options.task } : {},
2206
+ ...options.agent !== void 0 ? { agent: options.agent } : {}
2207
+ });
2208
+ if (reports.length === 0) {
2209
+ console.log("No reports stored yet. Run a pipeline to collect some.");
2210
+ return;
2211
+ }
2212
+ for (const r of reports) {
2213
+ console.log(`${r.id} ${r.agent.padEnd(10)} ${truncate(r.summary, 60)}`);
2214
+ }
2215
+ });
2216
+ report.command("combined").description("List combined reports (aggregated intelligence, \xA717).").option("--run <pipelineRunId>", "Only combined reports for this pipeline run.").action(async (options) => {
2217
+ const svc = service();
2218
+ const combined = await svc.storeRef.listCombined({
2219
+ ...options.run !== void 0 ? { pipelineRunId: options.run } : {}
2220
+ });
2221
+ if (combined.length === 0) {
2222
+ console.log("No combined reports stored yet. Run a pipeline to generate one.");
2223
+ return;
2224
+ }
2225
+ for (const c of combined) {
2226
+ console.log(
2227
+ `${c.id} ${c.findings.length} finding(s) from ${c.sources.length} source(s)${c.pipelineRunId ? ` run=${c.pipelineRunId}` : ""}`
2228
+ );
2229
+ }
2230
+ });
2231
+ report.command("show <id>").description("Show a stored report or combined report in full.").action(async (id) => {
2232
+ const svc = service();
2233
+ const reports = await svc.storeRef.listReports();
2234
+ const raw = reports.find((r) => r.id === id);
2235
+ if (raw) {
2236
+ console.log(`Report ${raw.id}`);
2237
+ console.log(` agent: ${raw.agent}${raw.model ? ` (${raw.model})` : ""}`);
2238
+ console.log(` task: ${raw.taskId}`);
2239
+ console.log(` summary: ${raw.summary}`);
2240
+ if (raw.findings.length > 0) {
2241
+ console.log(" findings:");
2242
+ for (const finding of raw.findings) {
2243
+ console.log(` - [${finding.severity ?? "info"}] ${finding.title}`);
2244
+ for (const evidence of finding.evidence ?? []) {
2245
+ console.log(` evidence: ${evidence}`);
2246
+ }
2247
+ }
2248
+ }
2249
+ if (raw.recommendations.length > 0) {
2250
+ console.log(" recommendations:");
2251
+ for (const recommendation of raw.recommendations) {
2252
+ console.log(` - ${recommendation}`);
2253
+ }
2254
+ }
2255
+ return;
2256
+ }
2257
+ const combined = await svc.storeRef.getCombined(id);
2258
+ if (combined) {
2259
+ console.log(`Combined report ${combined.id}`);
2260
+ console.log(` sources: ${combined.sources.join(", ")}`);
2261
+ console.log(` reports: ${combined.reportIds.length}`);
2262
+ if (combined.pipelineRunId) console.log(` run: ${combined.pipelineRunId}`);
2263
+ console.log(` summary: ${combined.summary}`);
2264
+ if (combined.findings.length > 0) {
2265
+ console.log(" findings (grouped, duplicates merged):");
2266
+ for (const finding of combined.findings) {
2267
+ console.log(
2268
+ ` - [${finding.severity ?? "info"}] ${finding.title} (x${finding.occurrences}, sources: ${finding.sources.map((s) => s.agent).join(", ")})`
2269
+ );
2270
+ }
2271
+ }
2272
+ if (combined.recommendations.length > 0) {
2273
+ console.log(" recommendations:");
2274
+ for (const recommendation of combined.recommendations) {
2275
+ console.log(` - ${recommendation.text} (x${recommendation.occurrences})`);
2276
+ }
2277
+ }
2278
+ return;
2279
+ }
2280
+ throw new OmniNodeError("REPORT_NOT_FOUND", `Report "${id}" was not found in the report store.`);
1580
2281
  });
1581
2282
  }
1582
2283
 
1583
2284
  // src/pipelines/engine.ts
1584
- import { randomBytes as randomBytes2 } from "crypto";
2285
+ import { randomBytes as randomBytes3 } from "crypto";
1585
2286
  var PipelineEngine = class {
1586
2287
  constructor(options) {
1587
2288
  this.options = options;
@@ -1694,7 +2395,7 @@ var PipelineEngine = class {
1694
2395
  }
1695
2396
  const now = (/* @__PURE__ */ new Date()).toISOString();
1696
2397
  const run2 = {
1697
- id: `run-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
2398
+ id: `run-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
1698
2399
  pipelineId: def.id,
1699
2400
  status: "running",
1700
2401
  startedAt: now,
@@ -1751,11 +2452,28 @@ ${contribution}`);
1751
2452
  }
1752
2453
  const failed = [...outcomes.values()].some((o) => o.status === "failed");
1753
2454
  const finalStatus = failed ? "failed" : "completed";
2455
+ let combinedReportId;
2456
+ if (this.options.reports) {
2457
+ const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
2458
+ const tasks = (await Promise.all(taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
2459
+ const collected = await this.options.reports.collectFromTasks(tasks);
2460
+ await this.options.reports.saveReports(collected);
2461
+ const combined = await this.options.reports.generateCombined(collected, {
2462
+ objective,
2463
+ pipelineRunId: run2.id,
2464
+ taskIds
2465
+ });
2466
+ combinedReportId = combined?.id;
2467
+ if (collected.length > 0) {
2468
+ run2.reports = collected;
2469
+ }
2470
+ }
1754
2471
  const finished = {
1755
2472
  ...run2,
1756
2473
  status: finalStatus,
1757
2474
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1758
- ...allReports.length > 0 ? { reports: allReports } : {}
2475
+ ...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
2476
+ ...combinedReportId !== void 0 ? { combinedReportId } : {}
1759
2477
  };
1760
2478
  await this.options.store.save(finished);
1761
2479
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
@@ -1935,8 +2653,8 @@ function createDefaultChatFn(providers) {
1935
2653
  }
1936
2654
 
1937
2655
  // src/pipelines/store.ts
1938
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1939
- import { dirname as dirname2 } from "path";
2656
+ import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
2657
+ import { dirname as dirname4 } from "path";
1940
2658
  var FilePipelineRunStore = class {
1941
2659
  filePath;
1942
2660
  queue = Promise.resolve();
@@ -1974,7 +2692,7 @@ var FilePipelineRunStore = class {
1974
2692
  }
1975
2693
  async load() {
1976
2694
  try {
1977
- const raw = await readFile2(this.filePath, "utf8");
2695
+ const raw = await readFile4(this.filePath, "utf8");
1978
2696
  const parsed = JSON.parse(raw);
1979
2697
  return Array.isArray(parsed) ? parsed : [];
1980
2698
  } catch {
@@ -1983,10 +2701,10 @@ var FilePipelineRunStore = class {
1983
2701
  }
1984
2702
  async write(runs) {
1985
2703
  const tmp = `${this.filePath}.tmp`;
1986
- await mkdir2(dirname2(this.filePath), { recursive: true });
1987
- await writeFile2(tmp, `${JSON.stringify(runs, null, 2)}
2704
+ await mkdir4(dirname4(this.filePath), { recursive: true });
2705
+ await writeFile4(tmp, `${JSON.stringify(runs, null, 2)}
1988
2706
  `, "utf8");
1989
- await rename2(tmp, this.filePath);
2707
+ await rename4(tmp, this.filePath);
1990
2708
  }
1991
2709
  };
1992
2710
 
@@ -1998,6 +2716,7 @@ function buildPipelineEngine(config, log = logger) {
1998
2716
  agents,
1999
2717
  roles,
2000
2718
  store: new FileTaskStore(),
2719
+ ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
2001
2720
  log
2002
2721
  });
2003
2722
  return new PipelineEngine({
@@ -2007,6 +2726,7 @@ function buildPipelineEngine(config, log = logger) {
2007
2726
  providers: config.project.providers,
2008
2727
  store: new FilePipelineRunStore(),
2009
2728
  chat: createDefaultChatFn(config.project.providers),
2729
+ reports: new ReportService(new FileReportStore(), log),
2010
2730
  log
2011
2731
  });
2012
2732
  }
@@ -2045,6 +2765,9 @@ function registerPipelineCommands(program) {
2045
2765
  if (finished.reports && finished.reports.length > 0) {
2046
2766
  console.log(` reports: ${finished.reports.length} structured report(s)`);
2047
2767
  }
2768
+ if (finished.combinedReportId) {
2769
+ console.log(` combined report: ${finished.combinedReportId}`);
2770
+ }
2048
2771
  console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
2049
2772
  if (finished.status !== "completed") process.exitCode = 1;
2050
2773
  });
@@ -2095,6 +2818,59 @@ function findPipeline(config, id) {
2095
2818
  return def;
2096
2819
  }
2097
2820
 
2821
+ // src/cli/commands/memory.ts
2822
+ function registerMemoryCommands(program) {
2823
+ const memory = program.command("memory").description("Inspect and manage persistent project memory (\xA719\u2013\xA720).");
2824
+ memory.command("status").description("Show the active memory provider and how much is stored.").action(async () => {
2825
+ const config = loadConfig();
2826
+ const configured = config.project.memory?.provider ?? "local (default \u2014 add a `memory:` section to opt in)";
2827
+ const svc = new MemoryService(createMemoryProvider(config.project.memory));
2828
+ const entries = await svc.providerRef.query({ limit: 1e4 });
2829
+ console.log(`provider: ${configured}`);
2830
+ console.log(`entries: ${entries.length}`);
2831
+ const scopes = /* @__PURE__ */ new Map();
2832
+ for (const entry2 of entries) {
2833
+ scopes.set(entry2.scope, (scopes.get(entry2.scope) ?? 0) + 1);
2834
+ }
2835
+ for (const [scope, count] of [...scopes.entries()].sort()) {
2836
+ console.log(` ${scope}: ${count}`);
2837
+ }
2838
+ });
2839
+ memory.command("query <text>").description("Search memory for entries relevant to the text.").option("-s, --scope <scope>", "Restrict to a scope: project, task, role or knowledge.").option("-n, --limit <count>", "Maximum entries to return.", (value) => Number(value), 10).action(async (text, options) => {
2840
+ const config = loadConfig();
2841
+ const svc = new MemoryService(createMemoryProvider(config.project.memory));
2842
+ const entries = await svc.providerRef.query({
2843
+ text,
2844
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
2845
+ limit: options.limit
2846
+ });
2847
+ if (entries.length === 0) {
2848
+ console.log("No relevant memory found.");
2849
+ return;
2850
+ }
2851
+ for (const entry2 of entries) {
2852
+ console.log(`[${entry2.scope}] ${entry2.key}`);
2853
+ console.log(` ${truncate(entry2.content.replace(/\n/g, " "), 120)}`);
2854
+ }
2855
+ });
2856
+ memory.command("write <text>").description("Write an entry to memory.").option("-s, --scope <scope>", "Memory scope: project, task, role or knowledge.", "project").option(
2857
+ "-t, --tags <tags>",
2858
+ "Comma-separated tags.",
2859
+ (value) => value.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0)
2860
+ ).option("-k, --key <key>", "Entry key (defaults to a generated note key).").action(async (text, options) => {
2861
+ const config = loadConfig();
2862
+ const svc = new MemoryService(createMemoryProvider(config.project.memory));
2863
+ const key = options.key ?? `note:${Date.now().toString(36)}`;
2864
+ await svc.providerRef.write({
2865
+ key,
2866
+ scope: options.scope,
2867
+ content: text,
2868
+ ...options.tags !== void 0 && options.tags.length > 0 ? { tags: options.tags } : {}
2869
+ });
2870
+ console.log(`Stored memory entry "${key}" (${options.scope}).`);
2871
+ });
2872
+ }
2873
+
2098
2874
  // src/cli/index.ts
2099
2875
  function createProgram() {
2100
2876
  const program = new Command();
@@ -2110,6 +2886,7 @@ function createProgram() {
2110
2886
  registerReportCommands(program);
2111
2887
  registerModelsCommand(program);
2112
2888
  registerPipelineCommands(program);
2889
+ registerMemoryCommands(program);
2113
2890
  return program;
2114
2891
  }
2115
2892
  async function run(argv) {