@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/README.md +31 -3
- package/dist/cli/index.js +798 -21
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +224 -12
- package/dist/index.js +657 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.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({
|
|
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 ? {
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
1483
|
-
roles
|
|
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)} ${
|
|
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
|
|
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 (
|
|
1578
|
-
report.command("list").description("List
|
|
1579
|
-
|
|
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
|
|
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)}-${
|
|
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
|
-
...
|
|
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
|
|
1939
|
-
import { dirname as
|
|
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
|
|
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
|
|
1987
|
-
await
|
|
2704
|
+
await mkdir4(dirname4(this.filePath), { recursive: true });
|
|
2705
|
+
await writeFile4(tmp, `${JSON.stringify(runs, null, 2)}
|
|
1988
2706
|
`, "utf8");
|
|
1989
|
-
await
|
|
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) {
|