@hilbras/omninode 0.5.2 → 0.6.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.6.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,
@@ -1216,23 +1258,42 @@ import { mkdir, readFile, rename, writeFile } from "fs/promises";
1216
1258
  import { dirname } from "path";
1217
1259
  var FileTaskStore = class {
1218
1260
  filePath;
1261
+ /**
1262
+ * Read-modify-write operations are serialized: parallel pipeline tasks
1263
+ * would otherwise race on the JSON file (last writer wins).
1264
+ */
1265
+ queue = Promise.resolve();
1219
1266
  constructor(directory = `${process.cwd()}/.omninode`) {
1220
1267
  this.filePath = `${directory}/tasks.json`;
1221
1268
  }
1222
1269
  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);
1270
+ return this.synchronized(async () => {
1271
+ const tasks = await this.load();
1272
+ const index = tasks.findIndex((t) => t.id === task.id);
1273
+ if (index >= 0) tasks[index] = task;
1274
+ else tasks.push(task);
1275
+ await this.write(tasks);
1276
+ });
1228
1277
  }
1229
1278
  async get(id) {
1230
- const tasks = await this.load();
1231
- return tasks.find((t) => t.id === id);
1279
+ return this.synchronized(async () => {
1280
+ const tasks = await this.load();
1281
+ return tasks.find((t) => t.id === id);
1282
+ });
1232
1283
  }
1233
1284
  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));
1285
+ return this.synchronized(async () => {
1286
+ const tasks = await this.load();
1287
+ 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
+ });
1289
+ }
1290
+ synchronized(operation) {
1291
+ const result = this.queue.then(operation, operation);
1292
+ this.queue = result.then(
1293
+ () => void 0,
1294
+ () => void 0
1295
+ );
1296
+ return result;
1236
1297
  }
1237
1298
  async load() {
1238
1299
  try {
@@ -1519,6 +1580,521 @@ function registerReportCommands(program) {
1519
1580
  });
1520
1581
  }
1521
1582
 
1583
+ // src/pipelines/engine.ts
1584
+ import { randomBytes as randomBytes2 } from "crypto";
1585
+ var PipelineEngine = class {
1586
+ constructor(options) {
1587
+ this.options = options;
1588
+ this.log = (options.log ?? logger).child({ component: "pipelines" });
1589
+ }
1590
+ options;
1591
+ log;
1592
+ get store() {
1593
+ return this.options.store;
1594
+ }
1595
+ /** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
1596
+ validate(def) {
1597
+ const ids = /* @__PURE__ */ new Set();
1598
+ for (const step of def.steps) {
1599
+ if (ids.has(step.id)) {
1600
+ throw new PipelineError("PIPELINE_INVALID", `Duplicate step id "${step.id}".`);
1601
+ }
1602
+ ids.add(step.id);
1603
+ }
1604
+ for (const step of def.steps) {
1605
+ for (const dep of step.dependsOn ?? []) {
1606
+ if (!ids.has(dep)) {
1607
+ throw new PipelineError(
1608
+ "PIPELINE_INVALID",
1609
+ `Step "${step.id}" depends on unknown step "${dep}".`
1610
+ );
1611
+ }
1612
+ }
1613
+ if (step.kind === "research" && (step.agents ?? []).length === 0) {
1614
+ throw new PipelineError(
1615
+ "PIPELINE_INVALID",
1616
+ `Research step "${step.id}" needs at least one agent.`
1617
+ );
1618
+ }
1619
+ if ((step.kind === "execute" || step.kind === "custom") && !step.agent) {
1620
+ throw new PipelineError("PIPELINE_INVALID", `Step "${step.id}" (${step.kind}) needs an agent.`);
1621
+ }
1622
+ }
1623
+ this.assertAcyclic(def);
1624
+ }
1625
+ assertAcyclic(def) {
1626
+ const deps = this.effectiveDependencies(def);
1627
+ const remaining = /* @__PURE__ */ new Map();
1628
+ const dependents = /* @__PURE__ */ new Map();
1629
+ for (const [id, list] of deps) {
1630
+ remaining.set(id, list.length);
1631
+ for (const d of list) {
1632
+ dependents.set(d, [...dependents.get(d) ?? [], id]);
1633
+ }
1634
+ }
1635
+ const queue = [...deps.keys()].filter((id) => (remaining.get(id) ?? 0) === 0);
1636
+ let processed = 0;
1637
+ while (queue.length > 0) {
1638
+ const id = queue.shift();
1639
+ processed += 1;
1640
+ for (const next of dependents.get(id) ?? []) {
1641
+ const left = (remaining.get(next) ?? 0) - 1;
1642
+ remaining.set(next, left);
1643
+ if (left === 0) queue.push(next);
1644
+ }
1645
+ }
1646
+ if (processed < deps.size) {
1647
+ const stuck = [...deps.keys()].filter((id) => (remaining.get(id) ?? 0) > 0);
1648
+ throw new PipelineError(
1649
+ "PIPELINE_INVALID",
1650
+ `Pipeline "${def.id}" has a dependency cycle involving: ${stuck.join(", ")}.`
1651
+ );
1652
+ }
1653
+ }
1654
+ /**
1655
+ * Steps run sequentially in definition order unless they declare explicit
1656
+ * dependencies, which enable parallel branches.
1657
+ */
1658
+ effectiveDependencies(def) {
1659
+ const deps = /* @__PURE__ */ new Map();
1660
+ def.steps.forEach((step, index) => {
1661
+ const explicit = step.dependsOn ?? [];
1662
+ deps.set(
1663
+ step.id,
1664
+ explicit.length > 0 ? explicit : index > 0 ? [def.steps[index - 1].id] : []
1665
+ );
1666
+ });
1667
+ return deps;
1668
+ }
1669
+ async run(def, options = {}) {
1670
+ this.validate(def);
1671
+ const objective = (options.objective ?? def.objective ?? "").trim();
1672
+ if (objective.length === 0) {
1673
+ throw new PipelineError(
1674
+ "PIPELINE_INVALID",
1675
+ `Pipeline "${def.id}" needs an objective \u2014 pass one or set it in the definition.`
1676
+ );
1677
+ }
1678
+ const role = def.role ? this.options.roles.get(def.role) : void 0;
1679
+ if (def.role && !role) {
1680
+ throw new PipelineError(
1681
+ "PIPELINE_INVALID",
1682
+ `Role "${def.role}" (pipeline "${def.id}") is not defined in project.roles.`
1683
+ );
1684
+ }
1685
+ for (const step of def.steps) {
1686
+ for (const name of [...step.agents ?? [], ...step.agent ? [step.agent] : []]) {
1687
+ if (!this.options.agents.get(name)) {
1688
+ throw new PipelineError(
1689
+ "PIPELINE_INVALID",
1690
+ `Step "${step.id}" references unregistered agent "${name}".`
1691
+ );
1692
+ }
1693
+ }
1694
+ }
1695
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1696
+ const run2 = {
1697
+ id: `run-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
1698
+ pipelineId: def.id,
1699
+ status: "running",
1700
+ startedAt: now,
1701
+ stepRuns: []
1702
+ };
1703
+ await this.options.store.save(run2);
1704
+ this.log.info(`Running pipeline ${def.id} as ${run2.id}.`);
1705
+ const deps = this.effectiveDependencies(def);
1706
+ const outcomes = /* @__PURE__ */ new Map();
1707
+ const pending = new Set(def.steps.map((s) => s.id));
1708
+ const contextParts = [];
1709
+ const allReports = [];
1710
+ while (pending.size > 0) {
1711
+ const ready = def.steps.filter(
1712
+ (step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
1713
+ );
1714
+ if (ready.length === 0) {
1715
+ throw new PipelineError("PIPELINE_FAILED", `Pipeline "${def.id}" scheduling stalled.`);
1716
+ }
1717
+ const settled = await Promise.all(
1718
+ ready.map(async (step) => {
1719
+ if (!this.shouldRun(step, deps.get(step.id) ?? [], outcomes)) {
1720
+ return { status: "skipped", summary: "", taskIds: [], reports: [] };
1721
+ }
1722
+ const ctx = {
1723
+ objective,
1724
+ ...role ? { role } : {},
1725
+ combinedContext: contextParts.join("\n\n")
1726
+ };
1727
+ return this.executeStep(step, ctx);
1728
+ })
1729
+ );
1730
+ ready.forEach((step, index) => {
1731
+ const outcome = settled[index];
1732
+ outcomes.set(step.id, outcome);
1733
+ pending.delete(step.id);
1734
+ allReports.push(...outcome.reports);
1735
+ if (outcome.status !== "skipped" && outcome.summary.length > 0) {
1736
+ const contribution = outcome.contextContribution ?? outcome.summary;
1737
+ contextParts.push(`## Step "${step.id}" (${step.kind})
1738
+ ${contribution}`);
1739
+ }
1740
+ const stepRun = {
1741
+ stepId: step.id,
1742
+ status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
1743
+ ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
1744
+ ...outcome.error !== void 0 ? { error: outcome.error } : {}
1745
+ };
1746
+ run2.stepRuns.push(stepRun);
1747
+ options.onStep?.(stepRun);
1748
+ });
1749
+ run2.reports = [...allReports];
1750
+ await this.options.store.save(run2);
1751
+ }
1752
+ const failed = [...outcomes.values()].some((o) => o.status === "failed");
1753
+ const finalStatus = failed ? "failed" : "completed";
1754
+ const finished = {
1755
+ ...run2,
1756
+ status: finalStatus,
1757
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1758
+ ...allReports.length > 0 ? { reports: allReports } : {}
1759
+ };
1760
+ await this.options.store.save(finished);
1761
+ this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
1762
+ return finished;
1763
+ }
1764
+ async get(runId) {
1765
+ return this.options.store.get(runId);
1766
+ }
1767
+ async listRuns(filter) {
1768
+ return this.options.store.list(filter);
1769
+ }
1770
+ shouldRun(step, deps, outcomes) {
1771
+ const condition = step.condition ?? "on-success";
1772
+ const depStatuses = deps.map((d) => outcomes.get(d)?.status);
1773
+ const anyFailed = depStatuses.includes("failed");
1774
+ const anySkipped = depStatuses.includes("skipped");
1775
+ if (condition === "always") return true;
1776
+ if (condition === "on-failure") return anyFailed;
1777
+ return !anyFailed && !anySkipped;
1778
+ }
1779
+ async executeStep(step, ctx) {
1780
+ const retries = step.retries ?? 0;
1781
+ let last;
1782
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
1783
+ if (attempt > 0) {
1784
+ this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
1785
+ }
1786
+ last = await this.executeStepOnce(step, ctx);
1787
+ if (last.status !== "failed") return last;
1788
+ }
1789
+ return last;
1790
+ }
1791
+ async executeStepOnce(step, ctx) {
1792
+ switch (step.kind) {
1793
+ case "research":
1794
+ return this.runFanOut(step, ctx);
1795
+ case "collect":
1796
+ return { status: "completed", summary: "(collect: outputs gathered from previous steps)", taskIds: [], reports: [] };
1797
+ case "analyze":
1798
+ if (!step.agent) {
1799
+ return { status: "completed", summary: "(analyze: pass-through, no agent assigned)", taskIds: [], reports: [] };
1800
+ }
1801
+ return this.runSingle(step, ctx, step.agent);
1802
+ case "plan":
1803
+ return this.runPlan(step, ctx);
1804
+ case "execute":
1805
+ case "custom":
1806
+ return this.runSingle(step, ctx, step.agent);
1807
+ }
1808
+ }
1809
+ /** Research fan-out: one task per agent, in parallel (§15). */
1810
+ async runFanOut(step, ctx) {
1811
+ const agentNames = step.agents ?? [];
1812
+ const created = await Promise.all(
1813
+ agentNames.map(
1814
+ (name) => this.options.tasks.create({
1815
+ objective: ctx.objective,
1816
+ agent: name,
1817
+ ...ctx.role ? { role: ctx.role.id } : {},
1818
+ ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
1819
+ })
1820
+ )
1821
+ );
1822
+ const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
1823
+ const failedTasks = results.filter((r) => r.status !== "completed");
1824
+ const lines = results.map((result, index) => {
1825
+ const agentName = agentNames[index] ?? "unknown";
1826
+ const outcome = result.result?.summary ?? result.result?.error ?? "(no output)";
1827
+ return `- ${agentName}: ${outcome}`;
1828
+ });
1829
+ const reports = results.flatMap((r) => r.result?.reports ?? []);
1830
+ const summary = lines.join("\n");
1831
+ if (failedTasks.length > 0) {
1832
+ const firstError = failedTasks[0]?.result?.error ?? "unknown error";
1833
+ return {
1834
+ status: "failed",
1835
+ summary,
1836
+ taskIds: created.map((t) => t.id),
1837
+ error: `${failedTasks.length}/${results.length} task(s) failed \u2014 ${firstError}`,
1838
+ reports
1839
+ };
1840
+ }
1841
+ return { status: "completed", summary, taskIds: created.map((t) => t.id), reports };
1842
+ }
1843
+ async runSingle(step, ctx, agentName) {
1844
+ const task = await this.options.tasks.create({
1845
+ objective: ctx.objective,
1846
+ agent: agentName,
1847
+ ...ctx.role ? { role: ctx.role.id } : {},
1848
+ ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
1849
+ });
1850
+ const result = await this.options.tasks.run(task.id);
1851
+ if (result.status !== "completed") {
1852
+ return {
1853
+ status: "failed",
1854
+ summary: result.result?.summary ?? "",
1855
+ taskIds: [task.id],
1856
+ error: result.result?.error ?? `task ${task.id} did not complete`,
1857
+ reports: result.result?.reports ?? []
1858
+ };
1859
+ }
1860
+ return {
1861
+ status: "completed",
1862
+ summary: result.result?.summary ?? "(no output)",
1863
+ taskIds: [task.id],
1864
+ reports: result.result?.reports ?? []
1865
+ };
1866
+ }
1867
+ async runPlan(step, ctx) {
1868
+ if (!step.model) {
1869
+ return { status: "completed", summary: "(plan: no model configured \u2014 skipped)", taskIds: [], reports: [] };
1870
+ }
1871
+ const messages = [
1872
+ {
1873
+ role: "system",
1874
+ content: "You are a planning assistant inside OmniNode. Given the objective and the combined research context, produce a prioritized, actionable plan."
1875
+ },
1876
+ {
1877
+ role: "user",
1878
+ content: `# Objective
1879
+ ${ctx.objective}
1880
+
1881
+ ${ctx.combinedContext}
1882
+
1883
+ Produce the final plan.`
1884
+ }
1885
+ ];
1886
+ try {
1887
+ const planText = await this.getChat()(step.model, messages);
1888
+ return {
1889
+ status: "completed",
1890
+ summary: planText.split("\n", 1)[0] ?? planText,
1891
+ contextContribution: planText,
1892
+ taskIds: [],
1893
+ reports: []
1894
+ };
1895
+ } catch (error) {
1896
+ return {
1897
+ status: "failed",
1898
+ summary: "",
1899
+ taskIds: [],
1900
+ error: error instanceof Error ? error.message : String(error),
1901
+ reports: []
1902
+ };
1903
+ }
1904
+ }
1905
+ getChat() {
1906
+ if (this.options.chat) return this.options.chat;
1907
+ throw new PipelineError(
1908
+ "PIPELINE_FAILED",
1909
+ "No chat function available \u2014 configure a provider for plan steps."
1910
+ );
1911
+ }
1912
+ };
1913
+ function createDefaultChatFn(providers) {
1914
+ return async (modelRef, messages) => {
1915
+ const separator = modelRef.indexOf(":");
1916
+ if (separator <= 0 || separator === modelRef.length - 1) {
1917
+ throw new PipelineError(
1918
+ "PIPELINE_INVALID",
1919
+ `Invalid model reference "${modelRef}". Use "provider:model-id".`
1920
+ );
1921
+ }
1922
+ const providerName = modelRef.slice(0, separator);
1923
+ const modelId = modelRef.slice(separator + 1);
1924
+ const providerConfig = providers.find((p) => p.name === providerName);
1925
+ if (!providerConfig) {
1926
+ throw new PipelineError(
1927
+ "PIPELINE_INVALID",
1928
+ `Provider "${providerName}" (model "${modelRef}") is not configured.`
1929
+ );
1930
+ }
1931
+ const provider = createProvider(providerConfig);
1932
+ const response = await provider.chat({ model: modelId, messages });
1933
+ return response.content;
1934
+ };
1935
+ }
1936
+
1937
+ // src/pipelines/store.ts
1938
+ import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1939
+ import { dirname as dirname2 } from "path";
1940
+ var FilePipelineRunStore = class {
1941
+ filePath;
1942
+ queue = Promise.resolve();
1943
+ constructor(directory = `${process.cwd()}/.omninode`) {
1944
+ this.filePath = `${directory}/pipelines.json`;
1945
+ }
1946
+ async save(run2) {
1947
+ return this.synchronized(async () => {
1948
+ const runs = await this.load();
1949
+ const index = runs.findIndex((r) => r.id === run2.id);
1950
+ if (index >= 0) runs[index] = run2;
1951
+ else runs.push(run2);
1952
+ await this.write(runs);
1953
+ });
1954
+ }
1955
+ async get(id) {
1956
+ return this.synchronized(async () => {
1957
+ const runs = await this.load();
1958
+ return runs.find((r) => r.id === id);
1959
+ });
1960
+ }
1961
+ async list(filter = {}) {
1962
+ return this.synchronized(async () => {
1963
+ const runs = await this.load();
1964
+ 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 ?? ""));
1965
+ });
1966
+ }
1967
+ synchronized(operation) {
1968
+ const result = this.queue.then(operation, operation);
1969
+ this.queue = result.then(
1970
+ () => void 0,
1971
+ () => void 0
1972
+ );
1973
+ return result;
1974
+ }
1975
+ async load() {
1976
+ try {
1977
+ const raw = await readFile2(this.filePath, "utf8");
1978
+ const parsed = JSON.parse(raw);
1979
+ return Array.isArray(parsed) ? parsed : [];
1980
+ } catch {
1981
+ return [];
1982
+ }
1983
+ }
1984
+ async write(runs) {
1985
+ const tmp = `${this.filePath}.tmp`;
1986
+ await mkdir2(dirname2(this.filePath), { recursive: true });
1987
+ await writeFile2(tmp, `${JSON.stringify(runs, null, 2)}
1988
+ `, "utf8");
1989
+ await rename2(tmp, this.filePath);
1990
+ }
1991
+ };
1992
+
1993
+ // src/pipelines/engine-factory.ts
1994
+ function buildPipelineEngine(config, log = logger) {
1995
+ const agents = buildAgentRegistry(config, log);
1996
+ const roles = buildRoleRegistry(config);
1997
+ const tasks = new TaskEngine({
1998
+ agents,
1999
+ roles,
2000
+ store: new FileTaskStore(),
2001
+ log
2002
+ });
2003
+ return new PipelineEngine({
2004
+ tasks,
2005
+ agents,
2006
+ roles,
2007
+ providers: config.project.providers,
2008
+ store: new FilePipelineRunStore(),
2009
+ chat: createDefaultChatFn(config.project.providers),
2010
+ log
2011
+ });
2012
+ }
2013
+
2014
+ // src/cli/commands/pipelines.ts
2015
+ function registerPipelineCommands(program) {
2016
+ const pipeline = program.command("pipeline").description("Run multi-step AI pipelines defined in omninode.yaml (\xA714).");
2017
+ pipeline.command("list").description("List configured pipelines.").action(async () => {
2018
+ const config = loadConfig();
2019
+ const pipelines = config.project.pipelines;
2020
+ if (pipelines.length === 0) {
2021
+ console.log("No pipelines configured. Add one under `project.pipelines` in omninode.yaml.");
2022
+ return;
2023
+ }
2024
+ for (const def of pipelines) {
2025
+ const name = def.name ? ` ${def.name}` : "";
2026
+ console.log(`${def.id}${name} ${def.steps.length} step(s)`);
2027
+ }
2028
+ });
2029
+ pipeline.command("run <id> [objective]").description(
2030
+ "Run a pipeline. The objective argument overrides the definition's default objective."
2031
+ ).action(async (id, objective) => {
2032
+ const config = loadConfig();
2033
+ const def = findPipeline(config, id);
2034
+ const engine = buildPipelineEngine(config);
2035
+ console.log(
2036
+ `Running pipeline "${def.id}" (${def.steps.length} step(s))${objective ? ` \u2014 "${objective}"` : ""}...`
2037
+ );
2038
+ const finished = await engine.run(def, {
2039
+ ...objective !== void 0 ? { objective } : {},
2040
+ onStep: (stepRun) => {
2041
+ const suffix = stepRun.error ? ` \u2014 ${stepRun.error}` : "";
2042
+ console.log(` [${stepRun.stepId}] ${stepRun.status}${suffix}`);
2043
+ }
2044
+ });
2045
+ if (finished.reports && finished.reports.length > 0) {
2046
+ console.log(` reports: ${finished.reports.length} structured report(s)`);
2047
+ }
2048
+ console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
2049
+ if (finished.status !== "completed") process.exitCode = 1;
2050
+ });
2051
+ pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
2052
+ const config = loadConfig();
2053
+ const engine = buildPipelineEngine(config);
2054
+ const runs = await engine.listRuns({ pipelineId: id });
2055
+ if (runs.length === 0) {
2056
+ console.log(`No recorded runs for pipeline "${id}".`);
2057
+ return;
2058
+ }
2059
+ for (const run2 of runs) {
2060
+ console.log(`${run2.id} ${run2.status.padEnd(10)} ${run2.startedAt ?? "?"}`);
2061
+ }
2062
+ });
2063
+ pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
2064
+ const config = loadConfig();
2065
+ const engine = buildPipelineEngine(config);
2066
+ const run2 = await engine.get(runId);
2067
+ if (!run2) {
2068
+ throw new PipelineError(
2069
+ "PIPELINE_NOT_FOUND",
2070
+ `Pipeline run "${runId}" does not exist in this project's run store.`
2071
+ );
2072
+ }
2073
+ console.log(` id: ${run2.id}`);
2074
+ console.log(` pipeline: ${run2.pipelineId}`);
2075
+ console.log(` status: ${run2.status}`);
2076
+ console.log(` started: ${run2.startedAt ?? "?"}`);
2077
+ if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
2078
+ for (const stepRun of run2.stepRuns) {
2079
+ console.log(` [${stepRun.stepId}] ${stepRun.status}`);
2080
+ }
2081
+ if (run2.reports && run2.reports.length > 0) {
2082
+ console.log(` reports: ${run2.reports.length}`);
2083
+ }
2084
+ });
2085
+ }
2086
+ function findPipeline(config, id) {
2087
+ const def = config.project.pipelines.find((p) => p.id === id);
2088
+ if (!def) {
2089
+ const available = config.project.pipelines.map((p) => p.id).join(", ") || "(none)";
2090
+ throw new PipelineError(
2091
+ "PIPELINE_NOT_FOUND",
2092
+ `Pipeline "${id}" is not configured. Configured pipelines: ${available}.`
2093
+ );
2094
+ }
2095
+ return def;
2096
+ }
2097
+
1522
2098
  // src/cli/index.ts
1523
2099
  function createProgram() {
1524
2100
  const program = new Command();
@@ -1533,6 +2109,7 @@ function createProgram() {
1533
2109
  registerTaskCommands(program);
1534
2110
  registerReportCommands(program);
1535
2111
  registerModelsCommand(program);
2112
+ registerPipelineCommands(program);
1536
2113
  return program;
1537
2114
  }
1538
2115
  async function run(argv) {