@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/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.5.2";
9
+ var OMNINODE_VERSION = "0.7.0";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -39,6 +39,11 @@ var TaskError = class extends OmniNodeError {
39
39
  super(code, message, options);
40
40
  }
41
41
  };
42
+ var PipelineError = class extends OmniNodeError {
43
+ constructor(code, message, options = {}) {
44
+ super(code, message, options);
45
+ }
46
+ };
42
47
  var ProtocolError = class extends OmniNodeError {
43
48
  constructor(message, options = {}) {
44
49
  super("PROTOCOL_VIOLATION", message, options);
@@ -84,11 +89,29 @@ var roleConfigSchema = z.object({
84
89
  system_prompt: z.string().optional(),
85
90
  metadata: z.record(z.unknown()).optional()
86
91
  }).strict();
92
+ var pipelineStepConfigSchema = z.object({
93
+ id: z.string().min(1),
94
+ kind: z.enum(["research", "collect", "analyze", "plan", "execute", "custom"]),
95
+ agents: z.array(z.string()).optional(),
96
+ agent: z.string().optional(),
97
+ model: z.string().optional(),
98
+ depends_on: z.array(z.string()).optional(),
99
+ retries: z.number().int().min(0).max(10).optional(),
100
+ condition: z.enum(["always", "on-success", "on-failure"]).optional()
101
+ }).strict();
102
+ var pipelineConfigSchema = z.object({
103
+ id: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "pipeline id must be kebab-case"),
104
+ name: z.string().optional(),
105
+ objective: z.string().optional(),
106
+ role: z.string().optional(),
107
+ steps: z.array(pipelineStepConfigSchema).min(1)
108
+ }).strict();
87
109
  var projectConfigSchema = z.object({
88
110
  name: z.string().min(1),
89
111
  providers: z.array(providerConfigSchema).default([]),
90
112
  agents: z.array(agentConfigSchema).default([]),
91
113
  roles: z.array(roleConfigSchema).default([]),
114
+ pipelines: z.array(pipelineConfigSchema).default([]),
92
115
  memory: z.object({ provider: z.string().min(1) }).strict().optional()
93
116
  }).strict();
94
117
  var appConfigSchema = z.object({
@@ -158,11 +181,30 @@ function toAppConfig(yaml) {
158
181
  providers: yaml.project.providers.map(toProviderConfig),
159
182
  agents: yaml.project.agents.map(toAgentConfig),
160
183
  roles: yaml.project.roles.map(toRoleDefinition),
184
+ pipelines: yaml.project.pipelines.map(toPipelineDefinition),
161
185
  ...yaml.project.memory !== void 0 ? { memory: yaml.project.memory } : {}
162
186
  },
163
187
  ...yaml.logging !== void 0 ? { logging: yaml.logging } : {}
164
188
  };
165
189
  }
190
+ function toPipelineDefinition(raw) {
191
+ return {
192
+ id: raw.id,
193
+ name: raw.name,
194
+ objective: raw.objective,
195
+ role: raw.role,
196
+ steps: raw.steps.map((step) => ({
197
+ id: step.id,
198
+ kind: step.kind,
199
+ agents: step.agents,
200
+ agent: step.agent,
201
+ model: step.model,
202
+ dependsOn: step.depends_on,
203
+ retries: step.retries,
204
+ condition: step.condition
205
+ }))
206
+ };
207
+ }
166
208
  function toProviderConfig(raw) {
167
209
  return {
168
210
  name: raw.name,
@@ -633,6 +675,9 @@ function notImplemented(command, when) {
633
675
  `${command} is planned for ${when}. Edit omninode.yaml directly for now.`
634
676
  );
635
677
  }
678
+ function truncate(text, max) {
679
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
680
+ }
636
681
  function appendToConfigList(listKey, item) {
637
682
  const configPath = findConfigFile();
638
683
  if (!configPath) {
@@ -1216,23 +1261,42 @@ import { mkdir, readFile, rename, writeFile } from "fs/promises";
1216
1261
  import { dirname } from "path";
1217
1262
  var FileTaskStore = class {
1218
1263
  filePath;
1264
+ /**
1265
+ * Read-modify-write operations are serialized: parallel pipeline tasks
1266
+ * would otherwise race on the JSON file (last writer wins).
1267
+ */
1268
+ queue = Promise.resolve();
1219
1269
  constructor(directory = `${process.cwd()}/.omninode`) {
1220
1270
  this.filePath = `${directory}/tasks.json`;
1221
1271
  }
1222
1272
  async save(task) {
1223
- const tasks = await this.load();
1224
- const index = tasks.findIndex((t) => t.id === task.id);
1225
- if (index >= 0) tasks[index] = task;
1226
- else tasks.push(task);
1227
- await this.write(tasks);
1273
+ return this.synchronized(async () => {
1274
+ const tasks = await this.load();
1275
+ const index = tasks.findIndex((t) => t.id === task.id);
1276
+ if (index >= 0) tasks[index] = task;
1277
+ else tasks.push(task);
1278
+ await this.write(tasks);
1279
+ });
1228
1280
  }
1229
1281
  async get(id) {
1230
- const tasks = await this.load();
1231
- return tasks.find((t) => t.id === id);
1282
+ return this.synchronized(async () => {
1283
+ const tasks = await this.load();
1284
+ return tasks.find((t) => t.id === id);
1285
+ });
1232
1286
  }
1233
1287
  async list(filter = {}) {
1234
- const tasks = await this.load();
1235
- 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));
1288
+ return this.synchronized(async () => {
1289
+ const tasks = await this.load();
1290
+ 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));
1291
+ });
1292
+ }
1293
+ synchronized(operation) {
1294
+ const result = this.queue.then(operation, operation);
1295
+ this.queue = result.then(
1296
+ () => void 0,
1297
+ () => void 0
1298
+ );
1299
+ return result;
1236
1300
  }
1237
1301
  async load() {
1238
1302
  try {
@@ -1343,6 +1407,8 @@ var TaskEngine = class {
1343
1407
  const result = {
1344
1408
  ...output.summary !== void 0 ? { summary: output.summary } : {},
1345
1409
  ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1410
+ // Keep the verbatim output (bounded) for report extraction and audit.
1411
+ ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1346
1412
  ...output.error !== void 0 ? { error: output.error } : {},
1347
1413
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1348
1414
  };
@@ -1477,7 +1543,7 @@ function registerTaskCommands(program) {
1477
1543
  return;
1478
1544
  }
1479
1545
  for (const t of tasks) {
1480
- console.log(`${t.id} ${t.status.padEnd(10)} ${truncate(t.objective, 60)}`);
1546
+ console.log(`${t.id} ${t.status.padEnd(10)} ${truncate2(t.objective, 60)}`);
1481
1547
  }
1482
1548
  });
1483
1549
  task.command("cancel <task-id>").description("Cancel a task that has not finished yet.").action(async (taskId) => {
@@ -1507,16 +1573,924 @@ function printOutcome(task, options = {}) {
1507
1573
  if (result.error) console.log(` error: ${result.error}`);
1508
1574
  if (result.finishedAt) console.log(` ended: ${result.finishedAt}`);
1509
1575
  }
1510
- function truncate(text, max) {
1576
+ function truncate2(text, max) {
1511
1577
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1512
1578
  }
1513
1579
 
1580
+ // src/reports/aggregate.ts
1581
+ import { randomBytes as randomBytes2 } from "crypto";
1582
+ var SEVERITY_RANK = {
1583
+ info: 0,
1584
+ low: 1,
1585
+ medium: 2,
1586
+ high: 3,
1587
+ critical: 4
1588
+ };
1589
+ function aggregateReports(reports, options = {}) {
1590
+ const threshold = options.similarityThreshold ?? 0.7;
1591
+ const findingGroups = [];
1592
+ const recommendationGroups = [];
1593
+ for (const report of reports) {
1594
+ for (const finding of report.findings) {
1595
+ mergeFinding(findingGroups, finding, report, threshold);
1596
+ }
1597
+ for (const text of report.recommendations) {
1598
+ mergeRecommendation(recommendationGroups, text, report, threshold);
1599
+ }
1600
+ }
1601
+ const sources = [...new Set(reports.map((r) => r.agent))].sort();
1602
+ const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
1603
+ const summary = `${reports.length} report(s) from ${sources.length > 0 ? sources.join(", ") : "no agents"}: ${findingGroups.length} unique finding group(s) (${totalRawFindings} raw), ${recommendationGroups.length} recommendation group(s).`;
1604
+ return {
1605
+ id: `combined-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
1606
+ title: options.objective !== void 0 ? truncate3(options.objective, 120) : "Combined report",
1607
+ ...options.objective !== void 0 ? { objective: options.objective } : {},
1608
+ ...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
1609
+ taskIds: options.taskIds ?? [],
1610
+ summary,
1611
+ findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
1612
+ recommendations: recommendationGroups,
1613
+ sources,
1614
+ reportIds: reports.map((r) => r.id),
1615
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1616
+ };
1617
+ }
1618
+ function rank(finding) {
1619
+ return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
1620
+ }
1621
+ function mergeFinding(groups, finding, report, threshold) {
1622
+ const source = { agent: report.agent, reportId: report.id };
1623
+ const group = groups.find((g) => similar(g.title, finding.title, threshold));
1624
+ if (group) {
1625
+ group.occurrences += 1;
1626
+ group.details.push(finding.detail);
1627
+ group.sources.push(source);
1628
+ group.evidence.push(...finding.evidence ?? []);
1629
+ if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
1630
+ group.severity = finding.severity;
1631
+ }
1632
+ return;
1633
+ }
1634
+ groups.push({
1635
+ title: finding.title,
1636
+ details: [finding.detail],
1637
+ sources: [source],
1638
+ occurrences: 1,
1639
+ ...finding.severity ? { severity: finding.severity } : {},
1640
+ evidence: [...finding.evidence ?? []]
1641
+ });
1642
+ }
1643
+ function mergeRecommendation(groups, text, report, threshold) {
1644
+ const source = { agent: report.agent, reportId: report.id };
1645
+ const group = groups.find((g) => similar(g.text, text, threshold));
1646
+ if (group) {
1647
+ group.occurrences += 1;
1648
+ group.sources.push(source);
1649
+ return;
1650
+ }
1651
+ groups.push({ text, sources: [source], occurrences: 1 });
1652
+ }
1653
+ function similar(a, b, threshold) {
1654
+ const left = tokens(a);
1655
+ const right = tokens(b);
1656
+ if (left.size === 0 || right.size === 0) return false;
1657
+ let intersection = 0;
1658
+ for (const token of left) {
1659
+ if (right.has(token)) intersection += 1;
1660
+ }
1661
+ const union = left.size + right.size - intersection;
1662
+ return intersection / union >= threshold;
1663
+ }
1664
+ function tokens(text) {
1665
+ return new Set(
1666
+ text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
1667
+ );
1668
+ }
1669
+ function truncate3(text, max) {
1670
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1671
+ }
1672
+
1673
+ // src/reports/extract.ts
1674
+ var BULLET = /^(?:[-*•]|\d+[.)])\s+(.*)$/;
1675
+ function parseSections(raw) {
1676
+ const sections = [];
1677
+ let current = { heading: "", bullets: [] };
1678
+ for (const line of raw.split("\n")) {
1679
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line.trim());
1680
+ if (heading) {
1681
+ if (current.heading.length > 0 || current.bullets.length > 0) {
1682
+ sections.push(current);
1683
+ }
1684
+ current = { heading: heading[2]?.toLowerCase() ?? "", bullets: [] };
1685
+ continue;
1686
+ }
1687
+ const bullet = BULLET.exec(line.trim());
1688
+ if (bullet && current.heading.length > 0) {
1689
+ current.bullets.push(bullet[1]?.trim() ?? "");
1690
+ }
1691
+ }
1692
+ if (current.heading.length > 0 || current.bullets.length > 0) {
1693
+ sections.push(current);
1694
+ }
1695
+ return sections;
1696
+ }
1697
+ function collectBullets(sections, patterns) {
1698
+ const matches = sections.filter((section) => patterns.some((p) => p.test(section.heading)));
1699
+ return matches.flatMap((section) => section.bullets).filter((b) => b.length > 0);
1700
+ }
1701
+ function guessSeverity(text) {
1702
+ if (/critical|severe/i.test(text)) return "critical";
1703
+ if (/\bhigh\b|major/i.test(text)) return "high";
1704
+ if (/medium|moderate/i.test(text)) return "medium";
1705
+ if (/\blow\b|minor/i.test(text)) return "low";
1706
+ return void 0;
1707
+ }
1708
+ function extractReportFromText(raw, meta) {
1709
+ const trimmed = raw.trim();
1710
+ const nonEmpty = trimmed.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1711
+ const proseLine = nonEmpty.find((line) => !line.startsWith("#") && !BULLET.test(line)) ?? nonEmpty[0] ?? "";
1712
+ const cleaned = proseLine.replace(/^#{1,6}\s+/, "").replace(BULLET, "$1");
1713
+ const summary = cleaned.length > 0 ? truncate4(cleaned, 200) : "(no output)";
1714
+ const sections = parseSections(trimmed);
1715
+ const findingTexts = collectBullets(sections, [/finding/, /issue/, /problem/, /vulnerab/]);
1716
+ const recommendationTexts = collectBullets(sections, [/recommend/, /suggestion/, /next step/, /action/]);
1717
+ const evidenceTexts = collectBullets(sections, [/evidence/, /proof/, /log/]);
1718
+ const findings = findingTexts.map((text, index) => ({
1719
+ id: `${meta.taskId}-f-${index + 1}`,
1720
+ title: truncate4(text, 120),
1721
+ detail: text,
1722
+ severity: guessSeverity(text)
1723
+ }));
1724
+ return {
1725
+ id: `${meta.taskId}-report`,
1726
+ taskId: meta.taskId,
1727
+ agent: meta.agent,
1728
+ ...meta.model !== void 0 ? { model: meta.model } : {},
1729
+ summary,
1730
+ findings,
1731
+ recommendations: recommendationTexts.map((t) => truncate4(t, 200)),
1732
+ metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
1733
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1734
+ };
1735
+ }
1736
+ function truncate4(text, max) {
1737
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1738
+ }
1739
+
1740
+ // src/reports/service.ts
1741
+ var ReportService = class {
1742
+ constructor(store, log = logger) {
1743
+ this.store = store;
1744
+ this.log = log.child({ component: "reports" });
1745
+ }
1746
+ store;
1747
+ log;
1748
+ get storeRef() {
1749
+ return this.store;
1750
+ }
1751
+ /** Normalizes finished tasks into reports (§17 — normalizer). */
1752
+ async collectFromTasks(tasks) {
1753
+ const reports = [];
1754
+ for (const task of tasks) {
1755
+ const structured = task.result?.reports ?? [];
1756
+ if (structured.length > 0) {
1757
+ reports.push(...structured);
1758
+ continue;
1759
+ }
1760
+ const text = task.result?.rawOutput ?? task.result?.summary ?? "";
1761
+ if (text.trim().length === 0) continue;
1762
+ reports.push(
1763
+ extractReportFromText(text, {
1764
+ taskId: task.id,
1765
+ agent: task.agent ?? "unknown"
1766
+ })
1767
+ );
1768
+ }
1769
+ return reports;
1770
+ }
1771
+ async saveReports(reports) {
1772
+ if (reports.length === 0) return;
1773
+ await this.store.saveReports(reports);
1774
+ this.log.info(`Stored ${reports.length} report(s).`);
1775
+ }
1776
+ async generateCombined(reports, meta = {}) {
1777
+ if (reports.length === 0) return void 0;
1778
+ const options = {
1779
+ ...meta.objective !== void 0 ? { objective: meta.objective } : {},
1780
+ ...meta.pipelineRunId !== void 0 ? { pipelineRunId: meta.pipelineRunId } : {},
1781
+ ...meta.taskIds !== void 0 ? { taskIds: meta.taskIds } : {}
1782
+ };
1783
+ const combined = aggregateReports(reports, options);
1784
+ await this.store.saveCombined(combined);
1785
+ this.log.info(`Generated combined report ${combined.id}.`);
1786
+ return combined;
1787
+ }
1788
+ };
1789
+
1790
+ // src/reports/store.ts
1791
+ import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1792
+ import { dirname as dirname2 } from "path";
1793
+ var FileReportStore = class {
1794
+ filePath;
1795
+ queue = Promise.resolve();
1796
+ constructor(directory = `${process.cwd()}/.omninode`) {
1797
+ this.filePath = `${directory}/reports.json`;
1798
+ }
1799
+ async saveReports(reports) {
1800
+ return this.synchronized(async () => {
1801
+ const entries = await this.load();
1802
+ for (const report of reports) {
1803
+ const index = entries.findIndex(
1804
+ (e) => e.kind === "report" && e.report.id === report.id
1805
+ );
1806
+ const entry2 = { kind: "report", report };
1807
+ if (index >= 0) entries[index] = entry2;
1808
+ else entries.push(entry2);
1809
+ }
1810
+ await this.write(entries);
1811
+ });
1812
+ }
1813
+ async listReports(filter = {}) {
1814
+ return this.synchronized(async () => {
1815
+ const entries = await this.load();
1816
+ return entries.filter((e) => e.kind === "report").map((e) => e.report).filter((report) => filter.taskId ? report.taskId === filter.taskId : true).filter((report) => filter.agent ? report.agent === filter.agent : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1817
+ });
1818
+ }
1819
+ async saveCombined(combined) {
1820
+ return this.synchronized(async () => {
1821
+ const entries = await this.load();
1822
+ const index = entries.findIndex(
1823
+ (e) => e.kind === "combined" && e.combined.id === combined.id
1824
+ );
1825
+ const entry2 = { kind: "combined", combined };
1826
+ if (index >= 0) entries[index] = entry2;
1827
+ else entries.push(entry2);
1828
+ await this.write(entries);
1829
+ });
1830
+ }
1831
+ async getCombined(id) {
1832
+ return this.synchronized(async () => {
1833
+ const entries = await this.load();
1834
+ const found = entries.find((e) => e.kind === "combined" && e.combined.id === id);
1835
+ return found?.kind === "combined" ? found.combined : void 0;
1836
+ });
1837
+ }
1838
+ async listCombined(filter = {}) {
1839
+ return this.synchronized(async () => {
1840
+ const entries = await this.load();
1841
+ return entries.filter((e) => e.kind === "combined").map((e) => e.combined).filter(
1842
+ (combined) => filter.pipelineRunId ? combined.pipelineRunId === filter.pipelineRunId : true
1843
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1844
+ });
1845
+ }
1846
+ synchronized(operation) {
1847
+ const result = this.queue.then(operation, operation);
1848
+ this.queue = result.then(
1849
+ () => void 0,
1850
+ () => void 0
1851
+ );
1852
+ return result;
1853
+ }
1854
+ async load() {
1855
+ try {
1856
+ const raw = await readFile2(this.filePath, "utf8");
1857
+ const parsed = JSON.parse(raw);
1858
+ return Array.isArray(parsed) ? parsed : [];
1859
+ } catch {
1860
+ return [];
1861
+ }
1862
+ }
1863
+ async write(entries) {
1864
+ const tmp = `${this.filePath}.tmp`;
1865
+ await mkdir2(dirname2(this.filePath), { recursive: true });
1866
+ await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
1867
+ `, "utf8");
1868
+ await rename2(tmp, this.filePath);
1869
+ }
1870
+ };
1871
+
1514
1872
  // src/cli/commands/reports.ts
1873
+ function service() {
1874
+ return new ReportService(new FileReportStore());
1875
+ }
1515
1876
  function registerReportCommands(program) {
1516
- const report = program.command("report").description("Inspect AI reports (Phase 6+).");
1517
- report.command("list").description("List collected reports (planned for Phase 6 \u2014 Multi-AI Report System).").action(() => {
1518
- throw notImplemented("omninode report list", "Phase 6 (Multi-AI Report System)");
1877
+ const report = program.command("report").description("Inspect AI reports collected from tasks and pipelines (\xA716\u2013\xA717).");
1878
+ report.command("list").description("List stored reports.").option("--task <taskId>", "Only reports for this task.").option("--agent <name>", "Only reports from this agent.").action(async (options) => {
1879
+ const svc = service();
1880
+ const reports = await svc.storeRef.listReports({
1881
+ ...options.task !== void 0 ? { taskId: options.task } : {},
1882
+ ...options.agent !== void 0 ? { agent: options.agent } : {}
1883
+ });
1884
+ if (reports.length === 0) {
1885
+ console.log("No reports stored yet. Run a pipeline to collect some.");
1886
+ return;
1887
+ }
1888
+ for (const r of reports) {
1889
+ console.log(`${r.id} ${r.agent.padEnd(10)} ${truncate(r.summary, 60)}`);
1890
+ }
1891
+ });
1892
+ report.command("combined").description("List combined reports (aggregated intelligence, \xA717).").option("--run <pipelineRunId>", "Only combined reports for this pipeline run.").action(async (options) => {
1893
+ const svc = service();
1894
+ const combined = await svc.storeRef.listCombined({
1895
+ ...options.run !== void 0 ? { pipelineRunId: options.run } : {}
1896
+ });
1897
+ if (combined.length === 0) {
1898
+ console.log("No combined reports stored yet. Run a pipeline to generate one.");
1899
+ return;
1900
+ }
1901
+ for (const c of combined) {
1902
+ console.log(
1903
+ `${c.id} ${c.findings.length} finding(s) from ${c.sources.length} source(s)${c.pipelineRunId ? ` run=${c.pipelineRunId}` : ""}`
1904
+ );
1905
+ }
1519
1906
  });
1907
+ report.command("show <id>").description("Show a stored report or combined report in full.").action(async (id) => {
1908
+ const svc = service();
1909
+ const reports = await svc.storeRef.listReports();
1910
+ const raw = reports.find((r) => r.id === id);
1911
+ if (raw) {
1912
+ console.log(`Report ${raw.id}`);
1913
+ console.log(` agent: ${raw.agent}${raw.model ? ` (${raw.model})` : ""}`);
1914
+ console.log(` task: ${raw.taskId}`);
1915
+ console.log(` summary: ${raw.summary}`);
1916
+ if (raw.findings.length > 0) {
1917
+ console.log(" findings:");
1918
+ for (const finding of raw.findings) {
1919
+ console.log(` - [${finding.severity ?? "info"}] ${finding.title}`);
1920
+ for (const evidence of finding.evidence ?? []) {
1921
+ console.log(` evidence: ${evidence}`);
1922
+ }
1923
+ }
1924
+ }
1925
+ if (raw.recommendations.length > 0) {
1926
+ console.log(" recommendations:");
1927
+ for (const recommendation of raw.recommendations) {
1928
+ console.log(` - ${recommendation}`);
1929
+ }
1930
+ }
1931
+ return;
1932
+ }
1933
+ const combined = await svc.storeRef.getCombined(id);
1934
+ if (combined) {
1935
+ console.log(`Combined report ${combined.id}`);
1936
+ console.log(` sources: ${combined.sources.join(", ")}`);
1937
+ console.log(` reports: ${combined.reportIds.length}`);
1938
+ if (combined.pipelineRunId) console.log(` run: ${combined.pipelineRunId}`);
1939
+ console.log(` summary: ${combined.summary}`);
1940
+ if (combined.findings.length > 0) {
1941
+ console.log(" findings (grouped, duplicates merged):");
1942
+ for (const finding of combined.findings) {
1943
+ console.log(
1944
+ ` - [${finding.severity ?? "info"}] ${finding.title} (x${finding.occurrences}, sources: ${finding.sources.map((s) => s.agent).join(", ")})`
1945
+ );
1946
+ }
1947
+ }
1948
+ if (combined.recommendations.length > 0) {
1949
+ console.log(" recommendations:");
1950
+ for (const recommendation of combined.recommendations) {
1951
+ console.log(` - ${recommendation.text} (x${recommendation.occurrences})`);
1952
+ }
1953
+ }
1954
+ return;
1955
+ }
1956
+ throw new OmniNodeError("REPORT_NOT_FOUND", `Report "${id}" was not found in the report store.`);
1957
+ });
1958
+ }
1959
+
1960
+ // src/pipelines/engine.ts
1961
+ import { randomBytes as randomBytes3 } from "crypto";
1962
+ var PipelineEngine = class {
1963
+ constructor(options) {
1964
+ this.options = options;
1965
+ this.log = (options.log ?? logger).child({ component: "pipelines" });
1966
+ }
1967
+ options;
1968
+ log;
1969
+ get store() {
1970
+ return this.options.store;
1971
+ }
1972
+ /** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
1973
+ validate(def) {
1974
+ const ids = /* @__PURE__ */ new Set();
1975
+ for (const step of def.steps) {
1976
+ if (ids.has(step.id)) {
1977
+ throw new PipelineError("PIPELINE_INVALID", `Duplicate step id "${step.id}".`);
1978
+ }
1979
+ ids.add(step.id);
1980
+ }
1981
+ for (const step of def.steps) {
1982
+ for (const dep of step.dependsOn ?? []) {
1983
+ if (!ids.has(dep)) {
1984
+ throw new PipelineError(
1985
+ "PIPELINE_INVALID",
1986
+ `Step "${step.id}" depends on unknown step "${dep}".`
1987
+ );
1988
+ }
1989
+ }
1990
+ if (step.kind === "research" && (step.agents ?? []).length === 0) {
1991
+ throw new PipelineError(
1992
+ "PIPELINE_INVALID",
1993
+ `Research step "${step.id}" needs at least one agent.`
1994
+ );
1995
+ }
1996
+ if ((step.kind === "execute" || step.kind === "custom") && !step.agent) {
1997
+ throw new PipelineError("PIPELINE_INVALID", `Step "${step.id}" (${step.kind}) needs an agent.`);
1998
+ }
1999
+ }
2000
+ this.assertAcyclic(def);
2001
+ }
2002
+ assertAcyclic(def) {
2003
+ const deps = this.effectiveDependencies(def);
2004
+ const remaining = /* @__PURE__ */ new Map();
2005
+ const dependents = /* @__PURE__ */ new Map();
2006
+ for (const [id, list] of deps) {
2007
+ remaining.set(id, list.length);
2008
+ for (const d of list) {
2009
+ dependents.set(d, [...dependents.get(d) ?? [], id]);
2010
+ }
2011
+ }
2012
+ const queue = [...deps.keys()].filter((id) => (remaining.get(id) ?? 0) === 0);
2013
+ let processed = 0;
2014
+ while (queue.length > 0) {
2015
+ const id = queue.shift();
2016
+ processed += 1;
2017
+ for (const next of dependents.get(id) ?? []) {
2018
+ const left = (remaining.get(next) ?? 0) - 1;
2019
+ remaining.set(next, left);
2020
+ if (left === 0) queue.push(next);
2021
+ }
2022
+ }
2023
+ if (processed < deps.size) {
2024
+ const stuck = [...deps.keys()].filter((id) => (remaining.get(id) ?? 0) > 0);
2025
+ throw new PipelineError(
2026
+ "PIPELINE_INVALID",
2027
+ `Pipeline "${def.id}" has a dependency cycle involving: ${stuck.join(", ")}.`
2028
+ );
2029
+ }
2030
+ }
2031
+ /**
2032
+ * Steps run sequentially in definition order unless they declare explicit
2033
+ * dependencies, which enable parallel branches.
2034
+ */
2035
+ effectiveDependencies(def) {
2036
+ const deps = /* @__PURE__ */ new Map();
2037
+ def.steps.forEach((step, index) => {
2038
+ const explicit = step.dependsOn ?? [];
2039
+ deps.set(
2040
+ step.id,
2041
+ explicit.length > 0 ? explicit : index > 0 ? [def.steps[index - 1].id] : []
2042
+ );
2043
+ });
2044
+ return deps;
2045
+ }
2046
+ async run(def, options = {}) {
2047
+ this.validate(def);
2048
+ const objective = (options.objective ?? def.objective ?? "").trim();
2049
+ if (objective.length === 0) {
2050
+ throw new PipelineError(
2051
+ "PIPELINE_INVALID",
2052
+ `Pipeline "${def.id}" needs an objective \u2014 pass one or set it in the definition.`
2053
+ );
2054
+ }
2055
+ const role = def.role ? this.options.roles.get(def.role) : void 0;
2056
+ if (def.role && !role) {
2057
+ throw new PipelineError(
2058
+ "PIPELINE_INVALID",
2059
+ `Role "${def.role}" (pipeline "${def.id}") is not defined in project.roles.`
2060
+ );
2061
+ }
2062
+ for (const step of def.steps) {
2063
+ for (const name of [...step.agents ?? [], ...step.agent ? [step.agent] : []]) {
2064
+ if (!this.options.agents.get(name)) {
2065
+ throw new PipelineError(
2066
+ "PIPELINE_INVALID",
2067
+ `Step "${step.id}" references unregistered agent "${name}".`
2068
+ );
2069
+ }
2070
+ }
2071
+ }
2072
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2073
+ const run2 = {
2074
+ id: `run-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
2075
+ pipelineId: def.id,
2076
+ status: "running",
2077
+ startedAt: now,
2078
+ stepRuns: []
2079
+ };
2080
+ await this.options.store.save(run2);
2081
+ this.log.info(`Running pipeline ${def.id} as ${run2.id}.`);
2082
+ const deps = this.effectiveDependencies(def);
2083
+ const outcomes = /* @__PURE__ */ new Map();
2084
+ const pending = new Set(def.steps.map((s) => s.id));
2085
+ const contextParts = [];
2086
+ const allReports = [];
2087
+ while (pending.size > 0) {
2088
+ const ready = def.steps.filter(
2089
+ (step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
2090
+ );
2091
+ if (ready.length === 0) {
2092
+ throw new PipelineError("PIPELINE_FAILED", `Pipeline "${def.id}" scheduling stalled.`);
2093
+ }
2094
+ const settled = await Promise.all(
2095
+ ready.map(async (step) => {
2096
+ if (!this.shouldRun(step, deps.get(step.id) ?? [], outcomes)) {
2097
+ return { status: "skipped", summary: "", taskIds: [], reports: [] };
2098
+ }
2099
+ const ctx = {
2100
+ objective,
2101
+ ...role ? { role } : {},
2102
+ combinedContext: contextParts.join("\n\n")
2103
+ };
2104
+ return this.executeStep(step, ctx);
2105
+ })
2106
+ );
2107
+ ready.forEach((step, index) => {
2108
+ const outcome = settled[index];
2109
+ outcomes.set(step.id, outcome);
2110
+ pending.delete(step.id);
2111
+ allReports.push(...outcome.reports);
2112
+ if (outcome.status !== "skipped" && outcome.summary.length > 0) {
2113
+ const contribution = outcome.contextContribution ?? outcome.summary;
2114
+ contextParts.push(`## Step "${step.id}" (${step.kind})
2115
+ ${contribution}`);
2116
+ }
2117
+ const stepRun = {
2118
+ stepId: step.id,
2119
+ status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
2120
+ ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
2121
+ ...outcome.error !== void 0 ? { error: outcome.error } : {}
2122
+ };
2123
+ run2.stepRuns.push(stepRun);
2124
+ options.onStep?.(stepRun);
2125
+ });
2126
+ run2.reports = [...allReports];
2127
+ await this.options.store.save(run2);
2128
+ }
2129
+ const failed = [...outcomes.values()].some((o) => o.status === "failed");
2130
+ const finalStatus = failed ? "failed" : "completed";
2131
+ let combinedReportId;
2132
+ if (this.options.reports) {
2133
+ const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
2134
+ const tasks = (await Promise.all(taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
2135
+ const collected = await this.options.reports.collectFromTasks(tasks);
2136
+ await this.options.reports.saveReports(collected);
2137
+ const combined = await this.options.reports.generateCombined(collected, {
2138
+ objective,
2139
+ pipelineRunId: run2.id,
2140
+ taskIds
2141
+ });
2142
+ combinedReportId = combined?.id;
2143
+ if (collected.length > 0) {
2144
+ run2.reports = collected;
2145
+ }
2146
+ }
2147
+ const finished = {
2148
+ ...run2,
2149
+ status: finalStatus,
2150
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
2151
+ ...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
2152
+ ...combinedReportId !== void 0 ? { combinedReportId } : {}
2153
+ };
2154
+ await this.options.store.save(finished);
2155
+ this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
2156
+ return finished;
2157
+ }
2158
+ async get(runId) {
2159
+ return this.options.store.get(runId);
2160
+ }
2161
+ async listRuns(filter) {
2162
+ return this.options.store.list(filter);
2163
+ }
2164
+ shouldRun(step, deps, outcomes) {
2165
+ const condition = step.condition ?? "on-success";
2166
+ const depStatuses = deps.map((d) => outcomes.get(d)?.status);
2167
+ const anyFailed = depStatuses.includes("failed");
2168
+ const anySkipped = depStatuses.includes("skipped");
2169
+ if (condition === "always") return true;
2170
+ if (condition === "on-failure") return anyFailed;
2171
+ return !anyFailed && !anySkipped;
2172
+ }
2173
+ async executeStep(step, ctx) {
2174
+ const retries = step.retries ?? 0;
2175
+ let last;
2176
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
2177
+ if (attempt > 0) {
2178
+ this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
2179
+ }
2180
+ last = await this.executeStepOnce(step, ctx);
2181
+ if (last.status !== "failed") return last;
2182
+ }
2183
+ return last;
2184
+ }
2185
+ async executeStepOnce(step, ctx) {
2186
+ switch (step.kind) {
2187
+ case "research":
2188
+ return this.runFanOut(step, ctx);
2189
+ case "collect":
2190
+ return { status: "completed", summary: "(collect: outputs gathered from previous steps)", taskIds: [], reports: [] };
2191
+ case "analyze":
2192
+ if (!step.agent) {
2193
+ return { status: "completed", summary: "(analyze: pass-through, no agent assigned)", taskIds: [], reports: [] };
2194
+ }
2195
+ return this.runSingle(step, ctx, step.agent);
2196
+ case "plan":
2197
+ return this.runPlan(step, ctx);
2198
+ case "execute":
2199
+ case "custom":
2200
+ return this.runSingle(step, ctx, step.agent);
2201
+ }
2202
+ }
2203
+ /** Research fan-out: one task per agent, in parallel (§15). */
2204
+ async runFanOut(step, ctx) {
2205
+ const agentNames = step.agents ?? [];
2206
+ const created = await Promise.all(
2207
+ agentNames.map(
2208
+ (name) => this.options.tasks.create({
2209
+ objective: ctx.objective,
2210
+ agent: name,
2211
+ ...ctx.role ? { role: ctx.role.id } : {},
2212
+ ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
2213
+ })
2214
+ )
2215
+ );
2216
+ const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
2217
+ const failedTasks = results.filter((r) => r.status !== "completed");
2218
+ const lines = results.map((result, index) => {
2219
+ const agentName = agentNames[index] ?? "unknown";
2220
+ const outcome = result.result?.summary ?? result.result?.error ?? "(no output)";
2221
+ return `- ${agentName}: ${outcome}`;
2222
+ });
2223
+ const reports = results.flatMap((r) => r.result?.reports ?? []);
2224
+ const summary = lines.join("\n");
2225
+ if (failedTasks.length > 0) {
2226
+ const firstError = failedTasks[0]?.result?.error ?? "unknown error";
2227
+ return {
2228
+ status: "failed",
2229
+ summary,
2230
+ taskIds: created.map((t) => t.id),
2231
+ error: `${failedTasks.length}/${results.length} task(s) failed \u2014 ${firstError}`,
2232
+ reports
2233
+ };
2234
+ }
2235
+ return { status: "completed", summary, taskIds: created.map((t) => t.id), reports };
2236
+ }
2237
+ async runSingle(step, ctx, agentName) {
2238
+ const task = await this.options.tasks.create({
2239
+ objective: ctx.objective,
2240
+ agent: agentName,
2241
+ ...ctx.role ? { role: ctx.role.id } : {},
2242
+ ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
2243
+ });
2244
+ const result = await this.options.tasks.run(task.id);
2245
+ if (result.status !== "completed") {
2246
+ return {
2247
+ status: "failed",
2248
+ summary: result.result?.summary ?? "",
2249
+ taskIds: [task.id],
2250
+ error: result.result?.error ?? `task ${task.id} did not complete`,
2251
+ reports: result.result?.reports ?? []
2252
+ };
2253
+ }
2254
+ return {
2255
+ status: "completed",
2256
+ summary: result.result?.summary ?? "(no output)",
2257
+ taskIds: [task.id],
2258
+ reports: result.result?.reports ?? []
2259
+ };
2260
+ }
2261
+ async runPlan(step, ctx) {
2262
+ if (!step.model) {
2263
+ return { status: "completed", summary: "(plan: no model configured \u2014 skipped)", taskIds: [], reports: [] };
2264
+ }
2265
+ const messages = [
2266
+ {
2267
+ role: "system",
2268
+ content: "You are a planning assistant inside OmniNode. Given the objective and the combined research context, produce a prioritized, actionable plan."
2269
+ },
2270
+ {
2271
+ role: "user",
2272
+ content: `# Objective
2273
+ ${ctx.objective}
2274
+
2275
+ ${ctx.combinedContext}
2276
+
2277
+ Produce the final plan.`
2278
+ }
2279
+ ];
2280
+ try {
2281
+ const planText = await this.getChat()(step.model, messages);
2282
+ return {
2283
+ status: "completed",
2284
+ summary: planText.split("\n", 1)[0] ?? planText,
2285
+ contextContribution: planText,
2286
+ taskIds: [],
2287
+ reports: []
2288
+ };
2289
+ } catch (error) {
2290
+ return {
2291
+ status: "failed",
2292
+ summary: "",
2293
+ taskIds: [],
2294
+ error: error instanceof Error ? error.message : String(error),
2295
+ reports: []
2296
+ };
2297
+ }
2298
+ }
2299
+ getChat() {
2300
+ if (this.options.chat) return this.options.chat;
2301
+ throw new PipelineError(
2302
+ "PIPELINE_FAILED",
2303
+ "No chat function available \u2014 configure a provider for plan steps."
2304
+ );
2305
+ }
2306
+ };
2307
+ function createDefaultChatFn(providers) {
2308
+ return async (modelRef, messages) => {
2309
+ const separator = modelRef.indexOf(":");
2310
+ if (separator <= 0 || separator === modelRef.length - 1) {
2311
+ throw new PipelineError(
2312
+ "PIPELINE_INVALID",
2313
+ `Invalid model reference "${modelRef}". Use "provider:model-id".`
2314
+ );
2315
+ }
2316
+ const providerName = modelRef.slice(0, separator);
2317
+ const modelId = modelRef.slice(separator + 1);
2318
+ const providerConfig = providers.find((p) => p.name === providerName);
2319
+ if (!providerConfig) {
2320
+ throw new PipelineError(
2321
+ "PIPELINE_INVALID",
2322
+ `Provider "${providerName}" (model "${modelRef}") is not configured.`
2323
+ );
2324
+ }
2325
+ const provider = createProvider(providerConfig);
2326
+ const response = await provider.chat({ model: modelId, messages });
2327
+ return response.content;
2328
+ };
2329
+ }
2330
+
2331
+ // src/pipelines/store.ts
2332
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
2333
+ import { dirname as dirname3 } from "path";
2334
+ var FilePipelineRunStore = class {
2335
+ filePath;
2336
+ queue = Promise.resolve();
2337
+ constructor(directory = `${process.cwd()}/.omninode`) {
2338
+ this.filePath = `${directory}/pipelines.json`;
2339
+ }
2340
+ async save(run2) {
2341
+ return this.synchronized(async () => {
2342
+ const runs = await this.load();
2343
+ const index = runs.findIndex((r) => r.id === run2.id);
2344
+ if (index >= 0) runs[index] = run2;
2345
+ else runs.push(run2);
2346
+ await this.write(runs);
2347
+ });
2348
+ }
2349
+ async get(id) {
2350
+ return this.synchronized(async () => {
2351
+ const runs = await this.load();
2352
+ return runs.find((r) => r.id === id);
2353
+ });
2354
+ }
2355
+ async list(filter = {}) {
2356
+ return this.synchronized(async () => {
2357
+ const runs = await this.load();
2358
+ return runs.filter((run2) => filter.pipelineId ? run2.pipelineId === filter.pipelineId : true).filter((run2) => filter.status ? run2.status === filter.status : true).sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? ""));
2359
+ });
2360
+ }
2361
+ synchronized(operation) {
2362
+ const result = this.queue.then(operation, operation);
2363
+ this.queue = result.then(
2364
+ () => void 0,
2365
+ () => void 0
2366
+ );
2367
+ return result;
2368
+ }
2369
+ async load() {
2370
+ try {
2371
+ const raw = await readFile3(this.filePath, "utf8");
2372
+ const parsed = JSON.parse(raw);
2373
+ return Array.isArray(parsed) ? parsed : [];
2374
+ } catch {
2375
+ return [];
2376
+ }
2377
+ }
2378
+ async write(runs) {
2379
+ const tmp = `${this.filePath}.tmp`;
2380
+ await mkdir3(dirname3(this.filePath), { recursive: true });
2381
+ await writeFile3(tmp, `${JSON.stringify(runs, null, 2)}
2382
+ `, "utf8");
2383
+ await rename3(tmp, this.filePath);
2384
+ }
2385
+ };
2386
+
2387
+ // src/pipelines/engine-factory.ts
2388
+ function buildPipelineEngine(config, log = logger) {
2389
+ const agents = buildAgentRegistry(config, log);
2390
+ const roles = buildRoleRegistry(config);
2391
+ const tasks = new TaskEngine({
2392
+ agents,
2393
+ roles,
2394
+ store: new FileTaskStore(),
2395
+ log
2396
+ });
2397
+ return new PipelineEngine({
2398
+ tasks,
2399
+ agents,
2400
+ roles,
2401
+ providers: config.project.providers,
2402
+ store: new FilePipelineRunStore(),
2403
+ chat: createDefaultChatFn(config.project.providers),
2404
+ reports: new ReportService(new FileReportStore(), log),
2405
+ log
2406
+ });
2407
+ }
2408
+
2409
+ // src/cli/commands/pipelines.ts
2410
+ function registerPipelineCommands(program) {
2411
+ const pipeline = program.command("pipeline").description("Run multi-step AI pipelines defined in omninode.yaml (\xA714).");
2412
+ pipeline.command("list").description("List configured pipelines.").action(async () => {
2413
+ const config = loadConfig();
2414
+ const pipelines = config.project.pipelines;
2415
+ if (pipelines.length === 0) {
2416
+ console.log("No pipelines configured. Add one under `project.pipelines` in omninode.yaml.");
2417
+ return;
2418
+ }
2419
+ for (const def of pipelines) {
2420
+ const name = def.name ? ` ${def.name}` : "";
2421
+ console.log(`${def.id}${name} ${def.steps.length} step(s)`);
2422
+ }
2423
+ });
2424
+ pipeline.command("run <id> [objective]").description(
2425
+ "Run a pipeline. The objective argument overrides the definition's default objective."
2426
+ ).action(async (id, objective) => {
2427
+ const config = loadConfig();
2428
+ const def = findPipeline(config, id);
2429
+ const engine = buildPipelineEngine(config);
2430
+ console.log(
2431
+ `Running pipeline "${def.id}" (${def.steps.length} step(s))${objective ? ` \u2014 "${objective}"` : ""}...`
2432
+ );
2433
+ const finished = await engine.run(def, {
2434
+ ...objective !== void 0 ? { objective } : {},
2435
+ onStep: (stepRun) => {
2436
+ const suffix = stepRun.error ? ` \u2014 ${stepRun.error}` : "";
2437
+ console.log(` [${stepRun.stepId}] ${stepRun.status}${suffix}`);
2438
+ }
2439
+ });
2440
+ if (finished.reports && finished.reports.length > 0) {
2441
+ console.log(` reports: ${finished.reports.length} structured report(s)`);
2442
+ }
2443
+ if (finished.combinedReportId) {
2444
+ console.log(` combined report: ${finished.combinedReportId}`);
2445
+ }
2446
+ console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
2447
+ if (finished.status !== "completed") process.exitCode = 1;
2448
+ });
2449
+ pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
2450
+ const config = loadConfig();
2451
+ const engine = buildPipelineEngine(config);
2452
+ const runs = await engine.listRuns({ pipelineId: id });
2453
+ if (runs.length === 0) {
2454
+ console.log(`No recorded runs for pipeline "${id}".`);
2455
+ return;
2456
+ }
2457
+ for (const run2 of runs) {
2458
+ console.log(`${run2.id} ${run2.status.padEnd(10)} ${run2.startedAt ?? "?"}`);
2459
+ }
2460
+ });
2461
+ pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
2462
+ const config = loadConfig();
2463
+ const engine = buildPipelineEngine(config);
2464
+ const run2 = await engine.get(runId);
2465
+ if (!run2) {
2466
+ throw new PipelineError(
2467
+ "PIPELINE_NOT_FOUND",
2468
+ `Pipeline run "${runId}" does not exist in this project's run store.`
2469
+ );
2470
+ }
2471
+ console.log(` id: ${run2.id}`);
2472
+ console.log(` pipeline: ${run2.pipelineId}`);
2473
+ console.log(` status: ${run2.status}`);
2474
+ console.log(` started: ${run2.startedAt ?? "?"}`);
2475
+ if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
2476
+ for (const stepRun of run2.stepRuns) {
2477
+ console.log(` [${stepRun.stepId}] ${stepRun.status}`);
2478
+ }
2479
+ if (run2.reports && run2.reports.length > 0) {
2480
+ console.log(` reports: ${run2.reports.length}`);
2481
+ }
2482
+ });
2483
+ }
2484
+ function findPipeline(config, id) {
2485
+ const def = config.project.pipelines.find((p) => p.id === id);
2486
+ if (!def) {
2487
+ const available = config.project.pipelines.map((p) => p.id).join(", ") || "(none)";
2488
+ throw new PipelineError(
2489
+ "PIPELINE_NOT_FOUND",
2490
+ `Pipeline "${id}" is not configured. Configured pipelines: ${available}.`
2491
+ );
2492
+ }
2493
+ return def;
1520
2494
  }
1521
2495
 
1522
2496
  // src/cli/index.ts
@@ -1533,6 +2507,7 @@ function createProgram() {
1533
2507
  registerTaskCommands(program);
1534
2508
  registerReportCommands(program);
1535
2509
  registerModelsCommand(program);
2510
+ registerPipelineCommands(program);
1536
2511
  return program;
1537
2512
  }
1538
2513
  async function run(argv) {