@hilbras/omninode 0.5.2 → 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/dist/index.js CHANGED
@@ -137,11 +137,29 @@ var roleConfigSchema = z.object({
137
137
  system_prompt: z.string().optional(),
138
138
  metadata: z.record(z.unknown()).optional()
139
139
  }).strict();
140
+ var pipelineStepConfigSchema = z.object({
141
+ id: z.string().min(1),
142
+ kind: z.enum(["research", "collect", "analyze", "plan", "execute", "custom"]),
143
+ agents: z.array(z.string()).optional(),
144
+ agent: z.string().optional(),
145
+ model: z.string().optional(),
146
+ depends_on: z.array(z.string()).optional(),
147
+ retries: z.number().int().min(0).max(10).optional(),
148
+ condition: z.enum(["always", "on-success", "on-failure"]).optional()
149
+ }).strict();
150
+ var pipelineConfigSchema = z.object({
151
+ id: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "pipeline id must be kebab-case"),
152
+ name: z.string().optional(),
153
+ objective: z.string().optional(),
154
+ role: z.string().optional(),
155
+ steps: z.array(pipelineStepConfigSchema).min(1)
156
+ }).strict();
140
157
  var projectConfigSchema = z.object({
141
158
  name: z.string().min(1),
142
159
  providers: z.array(providerConfigSchema).default([]),
143
160
  agents: z.array(agentConfigSchema).default([]),
144
161
  roles: z.array(roleConfigSchema).default([]),
162
+ pipelines: z.array(pipelineConfigSchema).default([]),
145
163
  memory: z.object({ provider: z.string().min(1) }).strict().optional()
146
164
  }).strict();
147
165
  var appConfigSchema = z.object({
@@ -211,11 +229,30 @@ function toAppConfig(yaml) {
211
229
  providers: yaml.project.providers.map(toProviderConfig),
212
230
  agents: yaml.project.agents.map(toAgentConfig),
213
231
  roles: yaml.project.roles.map(toRoleDefinition),
232
+ pipelines: yaml.project.pipelines.map(toPipelineDefinition),
214
233
  ...yaml.project.memory !== void 0 ? { memory: yaml.project.memory } : {}
215
234
  },
216
235
  ...yaml.logging !== void 0 ? { logging: yaml.logging } : {}
217
236
  };
218
237
  }
238
+ function toPipelineDefinition(raw) {
239
+ return {
240
+ id: raw.id,
241
+ name: raw.name,
242
+ objective: raw.objective,
243
+ role: raw.role,
244
+ steps: raw.steps.map((step) => ({
245
+ id: step.id,
246
+ kind: step.kind,
247
+ agents: step.agents,
248
+ agent: step.agent,
249
+ model: step.model,
250
+ dependsOn: step.depends_on,
251
+ retries: step.retries,
252
+ condition: step.condition
253
+ }))
254
+ };
255
+ }
219
256
  function toProviderConfig(raw) {
220
257
  return {
221
258
  name: raw.name,
@@ -914,23 +951,42 @@ import { mkdir, readFile, rename, writeFile } from "fs/promises";
914
951
  import { dirname } from "path";
915
952
  var FileTaskStore = class {
916
953
  filePath;
954
+ /**
955
+ * Read-modify-write operations are serialized: parallel pipeline tasks
956
+ * would otherwise race on the JSON file (last writer wins).
957
+ */
958
+ queue = Promise.resolve();
917
959
  constructor(directory = `${process.cwd()}/.omninode`) {
918
960
  this.filePath = `${directory}/tasks.json`;
919
961
  }
920
962
  async save(task) {
921
- const tasks = await this.load();
922
- const index = tasks.findIndex((t) => t.id === task.id);
923
- if (index >= 0) tasks[index] = task;
924
- else tasks.push(task);
925
- await this.write(tasks);
963
+ return this.synchronized(async () => {
964
+ const tasks = await this.load();
965
+ const index = tasks.findIndex((t) => t.id === task.id);
966
+ if (index >= 0) tasks[index] = task;
967
+ else tasks.push(task);
968
+ await this.write(tasks);
969
+ });
926
970
  }
927
971
  async get(id) {
928
- const tasks = await this.load();
929
- return tasks.find((t) => t.id === id);
972
+ return this.synchronized(async () => {
973
+ const tasks = await this.load();
974
+ return tasks.find((t) => t.id === id);
975
+ });
930
976
  }
931
977
  async list(filter = {}) {
932
- const tasks = await this.load();
933
- return tasks.filter((task) => filter.status ? task.status === filter.status : true).filter((task) => filter.project ? task.project === filter.project : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
978
+ return this.synchronized(async () => {
979
+ const tasks = await this.load();
980
+ return tasks.filter((task) => filter.status ? task.status === filter.status : true).filter((task) => filter.project ? task.project === filter.project : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
981
+ });
982
+ }
983
+ synchronized(operation) {
984
+ const result = this.queue.then(operation, operation);
985
+ this.queue = result.then(
986
+ () => void 0,
987
+ () => void 0
988
+ );
989
+ return result;
934
990
  }
935
991
  async load() {
936
992
  try {
@@ -1041,6 +1097,8 @@ var TaskEngine = class {
1041
1097
  const result = {
1042
1098
  ...output.summary !== void 0 ? { summary: output.summary } : {},
1043
1099
  ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1100
+ // Keep the verbatim output (bounded) for report extraction and audit.
1101
+ ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1044
1102
  ...output.error !== void 0 ? { error: output.error } : {},
1045
1103
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1046
1104
  };
@@ -1101,13 +1159,755 @@ function createTaskEngine(config, log = logger, directory) {
1101
1159
  });
1102
1160
  }
1103
1161
 
1162
+ // src/pipelines/engine.ts
1163
+ import { randomBytes as randomBytes2 } from "crypto";
1164
+ var PipelineEngine = class {
1165
+ constructor(options) {
1166
+ this.options = options;
1167
+ this.log = (options.log ?? logger).child({ component: "pipelines" });
1168
+ }
1169
+ options;
1170
+ log;
1171
+ get store() {
1172
+ return this.options.store;
1173
+ }
1174
+ /** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
1175
+ validate(def) {
1176
+ const ids = /* @__PURE__ */ new Set();
1177
+ for (const step of def.steps) {
1178
+ if (ids.has(step.id)) {
1179
+ throw new PipelineError("PIPELINE_INVALID", `Duplicate step id "${step.id}".`);
1180
+ }
1181
+ ids.add(step.id);
1182
+ }
1183
+ for (const step of def.steps) {
1184
+ for (const dep of step.dependsOn ?? []) {
1185
+ if (!ids.has(dep)) {
1186
+ throw new PipelineError(
1187
+ "PIPELINE_INVALID",
1188
+ `Step "${step.id}" depends on unknown step "${dep}".`
1189
+ );
1190
+ }
1191
+ }
1192
+ if (step.kind === "research" && (step.agents ?? []).length === 0) {
1193
+ throw new PipelineError(
1194
+ "PIPELINE_INVALID",
1195
+ `Research step "${step.id}" needs at least one agent.`
1196
+ );
1197
+ }
1198
+ if ((step.kind === "execute" || step.kind === "custom") && !step.agent) {
1199
+ throw new PipelineError("PIPELINE_INVALID", `Step "${step.id}" (${step.kind}) needs an agent.`);
1200
+ }
1201
+ }
1202
+ this.assertAcyclic(def);
1203
+ }
1204
+ assertAcyclic(def) {
1205
+ const deps = this.effectiveDependencies(def);
1206
+ const remaining = /* @__PURE__ */ new Map();
1207
+ const dependents = /* @__PURE__ */ new Map();
1208
+ for (const [id, list] of deps) {
1209
+ remaining.set(id, list.length);
1210
+ for (const d of list) {
1211
+ dependents.set(d, [...dependents.get(d) ?? [], id]);
1212
+ }
1213
+ }
1214
+ const queue = [...deps.keys()].filter((id) => (remaining.get(id) ?? 0) === 0);
1215
+ let processed = 0;
1216
+ while (queue.length > 0) {
1217
+ const id = queue.shift();
1218
+ processed += 1;
1219
+ for (const next of dependents.get(id) ?? []) {
1220
+ const left = (remaining.get(next) ?? 0) - 1;
1221
+ remaining.set(next, left);
1222
+ if (left === 0) queue.push(next);
1223
+ }
1224
+ }
1225
+ if (processed < deps.size) {
1226
+ const stuck = [...deps.keys()].filter((id) => (remaining.get(id) ?? 0) > 0);
1227
+ throw new PipelineError(
1228
+ "PIPELINE_INVALID",
1229
+ `Pipeline "${def.id}" has a dependency cycle involving: ${stuck.join(", ")}.`
1230
+ );
1231
+ }
1232
+ }
1233
+ /**
1234
+ * Steps run sequentially in definition order unless they declare explicit
1235
+ * dependencies, which enable parallel branches.
1236
+ */
1237
+ effectiveDependencies(def) {
1238
+ const deps = /* @__PURE__ */ new Map();
1239
+ def.steps.forEach((step, index) => {
1240
+ const explicit = step.dependsOn ?? [];
1241
+ deps.set(
1242
+ step.id,
1243
+ explicit.length > 0 ? explicit : index > 0 ? [def.steps[index - 1].id] : []
1244
+ );
1245
+ });
1246
+ return deps;
1247
+ }
1248
+ async run(def, options = {}) {
1249
+ this.validate(def);
1250
+ const objective = (options.objective ?? def.objective ?? "").trim();
1251
+ if (objective.length === 0) {
1252
+ throw new PipelineError(
1253
+ "PIPELINE_INVALID",
1254
+ `Pipeline "${def.id}" needs an objective \u2014 pass one or set it in the definition.`
1255
+ );
1256
+ }
1257
+ const role = def.role ? this.options.roles.get(def.role) : void 0;
1258
+ if (def.role && !role) {
1259
+ throw new PipelineError(
1260
+ "PIPELINE_INVALID",
1261
+ `Role "${def.role}" (pipeline "${def.id}") is not defined in project.roles.`
1262
+ );
1263
+ }
1264
+ for (const step of def.steps) {
1265
+ for (const name of [...step.agents ?? [], ...step.agent ? [step.agent] : []]) {
1266
+ if (!this.options.agents.get(name)) {
1267
+ throw new PipelineError(
1268
+ "PIPELINE_INVALID",
1269
+ `Step "${step.id}" references unregistered agent "${name}".`
1270
+ );
1271
+ }
1272
+ }
1273
+ }
1274
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1275
+ const run = {
1276
+ id: `run-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
1277
+ pipelineId: def.id,
1278
+ status: "running",
1279
+ startedAt: now,
1280
+ stepRuns: []
1281
+ };
1282
+ await this.options.store.save(run);
1283
+ this.log.info(`Running pipeline ${def.id} as ${run.id}.`);
1284
+ const deps = this.effectiveDependencies(def);
1285
+ const outcomes = /* @__PURE__ */ new Map();
1286
+ const pending = new Set(def.steps.map((s) => s.id));
1287
+ const contextParts = [];
1288
+ const allReports = [];
1289
+ while (pending.size > 0) {
1290
+ const ready = def.steps.filter(
1291
+ (step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
1292
+ );
1293
+ if (ready.length === 0) {
1294
+ throw new PipelineError("PIPELINE_FAILED", `Pipeline "${def.id}" scheduling stalled.`);
1295
+ }
1296
+ const settled = await Promise.all(
1297
+ ready.map(async (step) => {
1298
+ if (!this.shouldRun(step, deps.get(step.id) ?? [], outcomes)) {
1299
+ return { status: "skipped", summary: "", taskIds: [], reports: [] };
1300
+ }
1301
+ const ctx = {
1302
+ objective,
1303
+ ...role ? { role } : {},
1304
+ combinedContext: contextParts.join("\n\n")
1305
+ };
1306
+ return this.executeStep(step, ctx);
1307
+ })
1308
+ );
1309
+ ready.forEach((step, index) => {
1310
+ const outcome = settled[index];
1311
+ outcomes.set(step.id, outcome);
1312
+ pending.delete(step.id);
1313
+ allReports.push(...outcome.reports);
1314
+ if (outcome.status !== "skipped" && outcome.summary.length > 0) {
1315
+ const contribution = outcome.contextContribution ?? outcome.summary;
1316
+ contextParts.push(`## Step "${step.id}" (${step.kind})
1317
+ ${contribution}`);
1318
+ }
1319
+ const stepRun = {
1320
+ stepId: step.id,
1321
+ status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
1322
+ ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
1323
+ ...outcome.error !== void 0 ? { error: outcome.error } : {}
1324
+ };
1325
+ run.stepRuns.push(stepRun);
1326
+ options.onStep?.(stepRun);
1327
+ });
1328
+ run.reports = [...allReports];
1329
+ await this.options.store.save(run);
1330
+ }
1331
+ const failed = [...outcomes.values()].some((o) => o.status === "failed");
1332
+ const finalStatus = failed ? "failed" : "completed";
1333
+ let combinedReportId;
1334
+ if (this.options.reports) {
1335
+ const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
1336
+ const tasks = (await Promise.all(taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
1337
+ const collected = await this.options.reports.collectFromTasks(tasks);
1338
+ await this.options.reports.saveReports(collected);
1339
+ const combined = await this.options.reports.generateCombined(collected, {
1340
+ objective,
1341
+ pipelineRunId: run.id,
1342
+ taskIds
1343
+ });
1344
+ combinedReportId = combined?.id;
1345
+ if (collected.length > 0) {
1346
+ run.reports = collected;
1347
+ }
1348
+ }
1349
+ const finished = {
1350
+ ...run,
1351
+ status: finalStatus,
1352
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1353
+ ...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
1354
+ ...combinedReportId !== void 0 ? { combinedReportId } : {}
1355
+ };
1356
+ await this.options.store.save(finished);
1357
+ this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
1358
+ return finished;
1359
+ }
1360
+ async get(runId) {
1361
+ return this.options.store.get(runId);
1362
+ }
1363
+ async listRuns(filter) {
1364
+ return this.options.store.list(filter);
1365
+ }
1366
+ shouldRun(step, deps, outcomes) {
1367
+ const condition = step.condition ?? "on-success";
1368
+ const depStatuses = deps.map((d) => outcomes.get(d)?.status);
1369
+ const anyFailed = depStatuses.includes("failed");
1370
+ const anySkipped = depStatuses.includes("skipped");
1371
+ if (condition === "always") return true;
1372
+ if (condition === "on-failure") return anyFailed;
1373
+ return !anyFailed && !anySkipped;
1374
+ }
1375
+ async executeStep(step, ctx) {
1376
+ const retries = step.retries ?? 0;
1377
+ let last;
1378
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
1379
+ if (attempt > 0) {
1380
+ this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
1381
+ }
1382
+ last = await this.executeStepOnce(step, ctx);
1383
+ if (last.status !== "failed") return last;
1384
+ }
1385
+ return last;
1386
+ }
1387
+ async executeStepOnce(step, ctx) {
1388
+ switch (step.kind) {
1389
+ case "research":
1390
+ return this.runFanOut(step, ctx);
1391
+ case "collect":
1392
+ return { status: "completed", summary: "(collect: outputs gathered from previous steps)", taskIds: [], reports: [] };
1393
+ case "analyze":
1394
+ if (!step.agent) {
1395
+ return { status: "completed", summary: "(analyze: pass-through, no agent assigned)", taskIds: [], reports: [] };
1396
+ }
1397
+ return this.runSingle(step, ctx, step.agent);
1398
+ case "plan":
1399
+ return this.runPlan(step, ctx);
1400
+ case "execute":
1401
+ case "custom":
1402
+ return this.runSingle(step, ctx, step.agent);
1403
+ }
1404
+ }
1405
+ /** Research fan-out: one task per agent, in parallel (§15). */
1406
+ async runFanOut(step, ctx) {
1407
+ const agentNames = step.agents ?? [];
1408
+ const created = await Promise.all(
1409
+ agentNames.map(
1410
+ (name) => this.options.tasks.create({
1411
+ objective: ctx.objective,
1412
+ agent: name,
1413
+ ...ctx.role ? { role: ctx.role.id } : {},
1414
+ ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
1415
+ })
1416
+ )
1417
+ );
1418
+ const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
1419
+ const failedTasks = results.filter((r) => r.status !== "completed");
1420
+ const lines = results.map((result, index) => {
1421
+ const agentName = agentNames[index] ?? "unknown";
1422
+ const outcome = result.result?.summary ?? result.result?.error ?? "(no output)";
1423
+ return `- ${agentName}: ${outcome}`;
1424
+ });
1425
+ const reports = results.flatMap((r) => r.result?.reports ?? []);
1426
+ const summary = lines.join("\n");
1427
+ if (failedTasks.length > 0) {
1428
+ const firstError = failedTasks[0]?.result?.error ?? "unknown error";
1429
+ return {
1430
+ status: "failed",
1431
+ summary,
1432
+ taskIds: created.map((t) => t.id),
1433
+ error: `${failedTasks.length}/${results.length} task(s) failed \u2014 ${firstError}`,
1434
+ reports
1435
+ };
1436
+ }
1437
+ return { status: "completed", summary, taskIds: created.map((t) => t.id), reports };
1438
+ }
1439
+ async runSingle(step, ctx, agentName) {
1440
+ const task = await this.options.tasks.create({
1441
+ objective: ctx.objective,
1442
+ agent: agentName,
1443
+ ...ctx.role ? { role: ctx.role.id } : {},
1444
+ ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
1445
+ });
1446
+ const result = await this.options.tasks.run(task.id);
1447
+ if (result.status !== "completed") {
1448
+ return {
1449
+ status: "failed",
1450
+ summary: result.result?.summary ?? "",
1451
+ taskIds: [task.id],
1452
+ error: result.result?.error ?? `task ${task.id} did not complete`,
1453
+ reports: result.result?.reports ?? []
1454
+ };
1455
+ }
1456
+ return {
1457
+ status: "completed",
1458
+ summary: result.result?.summary ?? "(no output)",
1459
+ taskIds: [task.id],
1460
+ reports: result.result?.reports ?? []
1461
+ };
1462
+ }
1463
+ async runPlan(step, ctx) {
1464
+ if (!step.model) {
1465
+ return { status: "completed", summary: "(plan: no model configured \u2014 skipped)", taskIds: [], reports: [] };
1466
+ }
1467
+ const messages = [
1468
+ {
1469
+ role: "system",
1470
+ content: "You are a planning assistant inside OmniNode. Given the objective and the combined research context, produce a prioritized, actionable plan."
1471
+ },
1472
+ {
1473
+ role: "user",
1474
+ content: `# Objective
1475
+ ${ctx.objective}
1476
+
1477
+ ${ctx.combinedContext}
1478
+
1479
+ Produce the final plan.`
1480
+ }
1481
+ ];
1482
+ try {
1483
+ const planText = await this.getChat()(step.model, messages);
1484
+ return {
1485
+ status: "completed",
1486
+ summary: planText.split("\n", 1)[0] ?? planText,
1487
+ contextContribution: planText,
1488
+ taskIds: [],
1489
+ reports: []
1490
+ };
1491
+ } catch (error) {
1492
+ return {
1493
+ status: "failed",
1494
+ summary: "",
1495
+ taskIds: [],
1496
+ error: error instanceof Error ? error.message : String(error),
1497
+ reports: []
1498
+ };
1499
+ }
1500
+ }
1501
+ getChat() {
1502
+ if (this.options.chat) return this.options.chat;
1503
+ throw new PipelineError(
1504
+ "PIPELINE_FAILED",
1505
+ "No chat function available \u2014 configure a provider for plan steps."
1506
+ );
1507
+ }
1508
+ };
1509
+ function createDefaultChatFn(providers) {
1510
+ return async (modelRef, messages) => {
1511
+ const separator = modelRef.indexOf(":");
1512
+ if (separator <= 0 || separator === modelRef.length - 1) {
1513
+ throw new PipelineError(
1514
+ "PIPELINE_INVALID",
1515
+ `Invalid model reference "${modelRef}". Use "provider:model-id".`
1516
+ );
1517
+ }
1518
+ const providerName = modelRef.slice(0, separator);
1519
+ const modelId = modelRef.slice(separator + 1);
1520
+ const providerConfig = providers.find((p) => p.name === providerName);
1521
+ if (!providerConfig) {
1522
+ throw new PipelineError(
1523
+ "PIPELINE_INVALID",
1524
+ `Provider "${providerName}" (model "${modelRef}") is not configured.`
1525
+ );
1526
+ }
1527
+ const provider = createProvider(providerConfig);
1528
+ const response = await provider.chat({ model: modelId, messages });
1529
+ return response.content;
1530
+ };
1531
+ }
1532
+
1533
+ // src/pipelines/store.ts
1534
+ import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1535
+ import { dirname as dirname2 } from "path";
1536
+ var FilePipelineRunStore = class {
1537
+ filePath;
1538
+ queue = Promise.resolve();
1539
+ constructor(directory = `${process.cwd()}/.omninode`) {
1540
+ this.filePath = `${directory}/pipelines.json`;
1541
+ }
1542
+ async save(run) {
1543
+ return this.synchronized(async () => {
1544
+ const runs = await this.load();
1545
+ const index = runs.findIndex((r) => r.id === run.id);
1546
+ if (index >= 0) runs[index] = run;
1547
+ else runs.push(run);
1548
+ await this.write(runs);
1549
+ });
1550
+ }
1551
+ async get(id) {
1552
+ return this.synchronized(async () => {
1553
+ const runs = await this.load();
1554
+ return runs.find((r) => r.id === id);
1555
+ });
1556
+ }
1557
+ async list(filter = {}) {
1558
+ return this.synchronized(async () => {
1559
+ const runs = await this.load();
1560
+ return runs.filter((run) => filter.pipelineId ? run.pipelineId === filter.pipelineId : true).filter((run) => filter.status ? run.status === filter.status : true).sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? ""));
1561
+ });
1562
+ }
1563
+ synchronized(operation) {
1564
+ const result = this.queue.then(operation, operation);
1565
+ this.queue = result.then(
1566
+ () => void 0,
1567
+ () => void 0
1568
+ );
1569
+ return result;
1570
+ }
1571
+ async load() {
1572
+ try {
1573
+ const raw = await readFile2(this.filePath, "utf8");
1574
+ const parsed = JSON.parse(raw);
1575
+ return Array.isArray(parsed) ? parsed : [];
1576
+ } catch {
1577
+ return [];
1578
+ }
1579
+ }
1580
+ async write(runs) {
1581
+ const tmp = `${this.filePath}.tmp`;
1582
+ await mkdir2(dirname2(this.filePath), { recursive: true });
1583
+ await writeFile2(tmp, `${JSON.stringify(runs, null, 2)}
1584
+ `, "utf8");
1585
+ await rename2(tmp, this.filePath);
1586
+ }
1587
+ };
1588
+
1589
+ // src/reports/aggregate.ts
1590
+ import { randomBytes as randomBytes3 } from "crypto";
1591
+ var SEVERITY_RANK = {
1592
+ info: 0,
1593
+ low: 1,
1594
+ medium: 2,
1595
+ high: 3,
1596
+ critical: 4
1597
+ };
1598
+ function aggregateReports(reports, options = {}) {
1599
+ const threshold = options.similarityThreshold ?? 0.7;
1600
+ const findingGroups = [];
1601
+ const recommendationGroups = [];
1602
+ for (const report of reports) {
1603
+ for (const finding of report.findings) {
1604
+ mergeFinding(findingGroups, finding, report, threshold);
1605
+ }
1606
+ for (const text of report.recommendations) {
1607
+ mergeRecommendation(recommendationGroups, text, report, threshold);
1608
+ }
1609
+ }
1610
+ const sources = [...new Set(reports.map((r) => r.agent))].sort();
1611
+ const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
1612
+ 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).`;
1613
+ return {
1614
+ id: `combined-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
1615
+ title: options.objective !== void 0 ? truncate(options.objective, 120) : "Combined report",
1616
+ ...options.objective !== void 0 ? { objective: options.objective } : {},
1617
+ ...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
1618
+ taskIds: options.taskIds ?? [],
1619
+ summary,
1620
+ findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
1621
+ recommendations: recommendationGroups,
1622
+ sources,
1623
+ reportIds: reports.map((r) => r.id),
1624
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1625
+ };
1626
+ }
1627
+ function rank(finding) {
1628
+ return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
1629
+ }
1630
+ function mergeFinding(groups, finding, report, threshold) {
1631
+ const source = { agent: report.agent, reportId: report.id };
1632
+ const group = groups.find((g) => similar(g.title, finding.title, threshold));
1633
+ if (group) {
1634
+ group.occurrences += 1;
1635
+ group.details.push(finding.detail);
1636
+ group.sources.push(source);
1637
+ group.evidence.push(...finding.evidence ?? []);
1638
+ if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
1639
+ group.severity = finding.severity;
1640
+ }
1641
+ return;
1642
+ }
1643
+ groups.push({
1644
+ title: finding.title,
1645
+ details: [finding.detail],
1646
+ sources: [source],
1647
+ occurrences: 1,
1648
+ ...finding.severity ? { severity: finding.severity } : {},
1649
+ evidence: [...finding.evidence ?? []]
1650
+ });
1651
+ }
1652
+ function mergeRecommendation(groups, text, report, threshold) {
1653
+ const source = { agent: report.agent, reportId: report.id };
1654
+ const group = groups.find((g) => similar(g.text, text, threshold));
1655
+ if (group) {
1656
+ group.occurrences += 1;
1657
+ group.sources.push(source);
1658
+ return;
1659
+ }
1660
+ groups.push({ text, sources: [source], occurrences: 1 });
1661
+ }
1662
+ function similar(a, b, threshold) {
1663
+ const left = tokens(a);
1664
+ const right = tokens(b);
1665
+ if (left.size === 0 || right.size === 0) return false;
1666
+ let intersection = 0;
1667
+ for (const token of left) {
1668
+ if (right.has(token)) intersection += 1;
1669
+ }
1670
+ const union = left.size + right.size - intersection;
1671
+ return intersection / union >= threshold;
1672
+ }
1673
+ function tokens(text) {
1674
+ return new Set(
1675
+ text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
1676
+ );
1677
+ }
1678
+ function truncate(text, max) {
1679
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1680
+ }
1681
+
1682
+ // src/reports/extract.ts
1683
+ var BULLET = /^(?:[-*•]|\d+[.)])\s+(.*)$/;
1684
+ function parseSections(raw) {
1685
+ const sections = [];
1686
+ let current = { heading: "", bullets: [] };
1687
+ for (const line of raw.split("\n")) {
1688
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line.trim());
1689
+ if (heading) {
1690
+ if (current.heading.length > 0 || current.bullets.length > 0) {
1691
+ sections.push(current);
1692
+ }
1693
+ current = { heading: heading[2]?.toLowerCase() ?? "", bullets: [] };
1694
+ continue;
1695
+ }
1696
+ const bullet = BULLET.exec(line.trim());
1697
+ if (bullet && current.heading.length > 0) {
1698
+ current.bullets.push(bullet[1]?.trim() ?? "");
1699
+ }
1700
+ }
1701
+ if (current.heading.length > 0 || current.bullets.length > 0) {
1702
+ sections.push(current);
1703
+ }
1704
+ return sections;
1705
+ }
1706
+ function collectBullets(sections, patterns) {
1707
+ const matches = sections.filter((section) => patterns.some((p) => p.test(section.heading)));
1708
+ return matches.flatMap((section) => section.bullets).filter((b) => b.length > 0);
1709
+ }
1710
+ function guessSeverity(text) {
1711
+ if (/critical|severe/i.test(text)) return "critical";
1712
+ if (/\bhigh\b|major/i.test(text)) return "high";
1713
+ if (/medium|moderate/i.test(text)) return "medium";
1714
+ if (/\blow\b|minor/i.test(text)) return "low";
1715
+ return void 0;
1716
+ }
1717
+ function extractReportFromText(raw, meta) {
1718
+ const trimmed = raw.trim();
1719
+ const nonEmpty = trimmed.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1720
+ const proseLine = nonEmpty.find((line) => !line.startsWith("#") && !BULLET.test(line)) ?? nonEmpty[0] ?? "";
1721
+ const cleaned = proseLine.replace(/^#{1,6}\s+/, "").replace(BULLET, "$1");
1722
+ const summary = cleaned.length > 0 ? truncate2(cleaned, 200) : "(no output)";
1723
+ const sections = parseSections(trimmed);
1724
+ const findingTexts = collectBullets(sections, [/finding/, /issue/, /problem/, /vulnerab/]);
1725
+ const recommendationTexts = collectBullets(sections, [/recommend/, /suggestion/, /next step/, /action/]);
1726
+ const evidenceTexts = collectBullets(sections, [/evidence/, /proof/, /log/]);
1727
+ const findings = findingTexts.map((text, index) => ({
1728
+ id: `${meta.taskId}-f-${index + 1}`,
1729
+ title: truncate2(text, 120),
1730
+ detail: text,
1731
+ severity: guessSeverity(text)
1732
+ }));
1733
+ return {
1734
+ id: `${meta.taskId}-report`,
1735
+ taskId: meta.taskId,
1736
+ agent: meta.agent,
1737
+ ...meta.model !== void 0 ? { model: meta.model } : {},
1738
+ summary,
1739
+ findings,
1740
+ recommendations: recommendationTexts.map((t) => truncate2(t, 200)),
1741
+ metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
1742
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1743
+ };
1744
+ }
1745
+ function truncate2(text, max) {
1746
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1747
+ }
1748
+
1749
+ // src/reports/service.ts
1750
+ var ReportService = class {
1751
+ constructor(store, log = logger) {
1752
+ this.store = store;
1753
+ this.log = log.child({ component: "reports" });
1754
+ }
1755
+ store;
1756
+ log;
1757
+ get storeRef() {
1758
+ return this.store;
1759
+ }
1760
+ /** Normalizes finished tasks into reports (§17 — normalizer). */
1761
+ async collectFromTasks(tasks) {
1762
+ const reports = [];
1763
+ for (const task of tasks) {
1764
+ const structured = task.result?.reports ?? [];
1765
+ if (structured.length > 0) {
1766
+ reports.push(...structured);
1767
+ continue;
1768
+ }
1769
+ const text = task.result?.rawOutput ?? task.result?.summary ?? "";
1770
+ if (text.trim().length === 0) continue;
1771
+ reports.push(
1772
+ extractReportFromText(text, {
1773
+ taskId: task.id,
1774
+ agent: task.agent ?? "unknown"
1775
+ })
1776
+ );
1777
+ }
1778
+ return reports;
1779
+ }
1780
+ async saveReports(reports) {
1781
+ if (reports.length === 0) return;
1782
+ await this.store.saveReports(reports);
1783
+ this.log.info(`Stored ${reports.length} report(s).`);
1784
+ }
1785
+ async generateCombined(reports, meta = {}) {
1786
+ if (reports.length === 0) return void 0;
1787
+ const options = {
1788
+ ...meta.objective !== void 0 ? { objective: meta.objective } : {},
1789
+ ...meta.pipelineRunId !== void 0 ? { pipelineRunId: meta.pipelineRunId } : {},
1790
+ ...meta.taskIds !== void 0 ? { taskIds: meta.taskIds } : {}
1791
+ };
1792
+ const combined = aggregateReports(reports, options);
1793
+ await this.store.saveCombined(combined);
1794
+ this.log.info(`Generated combined report ${combined.id}.`);
1795
+ return combined;
1796
+ }
1797
+ };
1798
+
1799
+ // src/reports/store.ts
1800
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
1801
+ import { dirname as dirname3 } from "path";
1802
+ var FileReportStore = class {
1803
+ filePath;
1804
+ queue = Promise.resolve();
1805
+ constructor(directory = `${process.cwd()}/.omninode`) {
1806
+ this.filePath = `${directory}/reports.json`;
1807
+ }
1808
+ async saveReports(reports) {
1809
+ return this.synchronized(async () => {
1810
+ const entries = await this.load();
1811
+ for (const report of reports) {
1812
+ const index = entries.findIndex(
1813
+ (e) => e.kind === "report" && e.report.id === report.id
1814
+ );
1815
+ const entry = { kind: "report", report };
1816
+ if (index >= 0) entries[index] = entry;
1817
+ else entries.push(entry);
1818
+ }
1819
+ await this.write(entries);
1820
+ });
1821
+ }
1822
+ async listReports(filter = {}) {
1823
+ return this.synchronized(async () => {
1824
+ const entries = await this.load();
1825
+ 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));
1826
+ });
1827
+ }
1828
+ async saveCombined(combined) {
1829
+ return this.synchronized(async () => {
1830
+ const entries = await this.load();
1831
+ const index = entries.findIndex(
1832
+ (e) => e.kind === "combined" && e.combined.id === combined.id
1833
+ );
1834
+ const entry = { kind: "combined", combined };
1835
+ if (index >= 0) entries[index] = entry;
1836
+ else entries.push(entry);
1837
+ await this.write(entries);
1838
+ });
1839
+ }
1840
+ async getCombined(id) {
1841
+ return this.synchronized(async () => {
1842
+ const entries = await this.load();
1843
+ const found = entries.find((e) => e.kind === "combined" && e.combined.id === id);
1844
+ return found?.kind === "combined" ? found.combined : void 0;
1845
+ });
1846
+ }
1847
+ async listCombined(filter = {}) {
1848
+ return this.synchronized(async () => {
1849
+ const entries = await this.load();
1850
+ return entries.filter((e) => e.kind === "combined").map((e) => e.combined).filter(
1851
+ (combined) => filter.pipelineRunId ? combined.pipelineRunId === filter.pipelineRunId : true
1852
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1853
+ });
1854
+ }
1855
+ synchronized(operation) {
1856
+ const result = this.queue.then(operation, operation);
1857
+ this.queue = result.then(
1858
+ () => void 0,
1859
+ () => void 0
1860
+ );
1861
+ return result;
1862
+ }
1863
+ async load() {
1864
+ try {
1865
+ const raw = await readFile3(this.filePath, "utf8");
1866
+ const parsed = JSON.parse(raw);
1867
+ return Array.isArray(parsed) ? parsed : [];
1868
+ } catch {
1869
+ return [];
1870
+ }
1871
+ }
1872
+ async write(entries) {
1873
+ const tmp = `${this.filePath}.tmp`;
1874
+ await mkdir3(dirname3(this.filePath), { recursive: true });
1875
+ await writeFile3(tmp, `${JSON.stringify(entries, null, 2)}
1876
+ `, "utf8");
1877
+ await rename3(tmp, this.filePath);
1878
+ }
1879
+ };
1880
+
1881
+ // src/pipelines/engine-factory.ts
1882
+ function buildPipelineEngine(config, log = logger) {
1883
+ const agents = buildAgentRegistry(config, log);
1884
+ const roles = buildRoleRegistry(config);
1885
+ const tasks = new TaskEngine({
1886
+ agents,
1887
+ roles,
1888
+ store: new FileTaskStore(),
1889
+ log
1890
+ });
1891
+ return new PipelineEngine({
1892
+ tasks,
1893
+ agents,
1894
+ roles,
1895
+ providers: config.project.providers,
1896
+ store: new FilePipelineRunStore(),
1897
+ chat: createDefaultChatFn(config.project.providers),
1898
+ reports: new ReportService(new FileReportStore(), log),
1899
+ log
1900
+ });
1901
+ }
1902
+
1104
1903
  // src/version.ts
1105
- var OMNINODE_VERSION = "0.5.2";
1904
+ var OMNINODE_VERSION = "0.7.0";
1106
1905
  export {
1107
1906
  AgentError,
1108
1907
  AgentRegistry,
1109
1908
  ConfigError,
1110
1909
  ConsoleLogSink,
1910
+ FilePipelineRunStore,
1111
1911
  FileTaskStore,
1112
1912
  Logger,
1113
1913
  MemoryError,
@@ -1117,6 +1917,7 @@ export {
1117
1917
  OmniHilbrasProvider,
1118
1918
  OmniNodeError,
1119
1919
  OpenAICompatibleProvider,
1920
+ PipelineEngine,
1120
1921
  PipelineError,
1121
1922
  ProcessAgent,
1122
1923
  ProtocolError,
@@ -1128,9 +1929,11 @@ export {
1128
1929
  appConfigSchema,
1129
1930
  authHeaders,
1130
1931
  buildAgentRegistry,
1932
+ buildPipelineEngine,
1131
1933
  buildRoleRegistry,
1132
1934
  composeTaskText,
1133
1935
  createAgent,
1936
+ createDefaultChatFn,
1134
1937
  createProvider,
1135
1938
  createTaskEngine,
1136
1939
  defaultProjectConfigYaml,
@@ -1139,6 +1942,8 @@ export {
1139
1942
  isOmniNodeError,
1140
1943
  loadConfig,
1141
1944
  logger,
1945
+ pipelineConfigSchema,
1946
+ pipelineStepConfigSchema,
1142
1947
  projectConfigSchema,
1143
1948
  providerConfigSchema,
1144
1949
  registryKey,