@hilbras/omninode 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,8 +18,26 @@ existing intelligence; it does not try to become another model.
18
18
 
19
19
  ## Status
20
20
 
21
- **v0.9.0 — Phase 8: Planner System** (see [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md)
22
- for the full plan and roadmap).
21
+ **v1.0.0 — Phases 10 + 11: Production Release.** The plan is complete: the
22
+ full loop from §28 runs in one command —
23
+ `User → project context → memory → research agents → reports → aggregation →
24
+ planner → plan → execution → result` — with audit logging, error recovery and
25
+ production hardening (see [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md)
26
+ for the plan and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design).
27
+
28
+ ### What's in v1.0
29
+
30
+ | Capability | Where |
31
+ | --- | --- |
32
+ | Provider abstraction, custom providers, model discovery | `src/providers/`, `src/registry/` |
33
+ | OmniHilbras integration (optional) | `src/providers/omnihilbras/` |
34
+ | CLI agents (stdin/arg/protocol), agent registry | `src/agents/` |
35
+ | Roles, projects, tasks (lifecycle + retry) | `src/roles/`, `src/tasks/` |
36
+ | Pipelines (DAG, parallel, retries, conditions) | `src/pipelines/` |
37
+ | Multi-AI research + report aggregation | `src/reports/` |
38
+ | Planner layer (model + heuristic) | `src/planner/` |
39
+ | Remembera / local memory | `src/memory/` |
40
+ | CLI, configuration, security foundations, docs | `src/cli/`, `src/config/`, `docs/` |
23
41
 
24
42
  What exists today:
25
43
 
@@ -97,6 +115,15 @@ What exists today:
97
115
  - Plans persist to `.omninode/plans.json`; pipeline runs record `planId`
98
116
  - Configure with `planner: { kind: model | heuristic, model, instruction }`
99
117
  — never hardcoded to any provider
118
+ - **End-to-end workflow** (§28, §27 Phase 9):
119
+ - One-command execution via the top-level `run` alias (`omninode run <pipeline> "<objective>"`)
120
+ - `omninode status` — full project state overview: providers, agents,
121
+ roles, pipelines, task/run/report/plan counts, memory provider + entries
122
+ - Pipeline runs persist their objective and a final `resultSummary` (§28 "Result")
123
+ - Error recovery: `task retry [--run]` (reset a failed/cancelled task) and
124
+ `pipeline retry <run-id>` (re-run a recorded failed run as a new run)
125
+ - Full integration test (`tests/e2e.test.ts`) drives the entire §28 workflow
126
+ through the real CLI against a local gateway
100
127
  - Typed error hierarchy (`src/errors/`)
101
128
  - Leveled logger with pluggable sink (`src/logger/`)
102
129
  - Configuration system: `omninode.yaml` with `${ENV_VAR}` expansion and strict
@@ -190,6 +217,14 @@ project:
190
217
  model: my-gateway:chatgpt
191
218
  ```
192
219
 
220
+ ```bash
221
+ omninode init
222
+ omninode status
223
+ omninode run repo-audit "Audit the authentication module" # the whole workflow
224
+ omninode pipeline retry run-xxxx # recover a failed run
225
+ omninode task retry task-yyyy --run # recover a failed task
226
+ ```
227
+
193
228
  Persistent memory (§19–§20) — opt in with `memory: { provider: local }` or
194
229
  `provider: remembera` in `omninode.yaml`:
195
230
 
@@ -233,17 +268,27 @@ Phases 0–11 are laid out in [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.m
233
268
  | v0.3.0 | OmniHilbras Integration (optional, like every provider) |
234
269
  | v0.4.0 | Agent System |
235
270
  | v0.5.0 | Roles & Tasks |
236
- | v0.7.0 | Multi-AI Reports |
271
+ | v0.6.0 | Pipeline Engine |
237
272
  | v0.7.0 | Multi-AI Reports |
238
273
  | v0.8.0 | Remembera / Memory |
239
- | **v0.9.0** | **Planner (current)** |
240
- | v0.10.0 | End-to-End Runtime |
241
- | v1.0.0 | Production Release |
274
+ | v0.9.0 | Planner |
275
+ | v0.10.0 | End-to-End Workflow (current) |
276
+ | **v1.0.0** | **Production Release — Phases 10 + 11 (current, final)** |
242
277
 
243
278
  Core architectural rule: OmniNode must run successfully with **zero Hilbras
244
279
  dependencies**. OmniHilbras and Remembera make it more powerful, but neither is
245
280
  required for the core engine.
246
281
 
282
+ ## Documentation
283
+
284
+ - [Development plan](docs/DEVELOPMENT_PLAN.md) — the full 12-phase roadmap
285
+ - [Architecture](docs/ARCHITECTURE.md) — module map, workflow, local state
286
+ - [Security model](docs/SECURITY.md) — secrets, process execution, env isolation, audit log
287
+ - [Example project](examples/demo/) — a runnable end-to-end demo (`./demo.sh`)
288
+
289
+ Prefer a container? `docker build -t omninode .` then
290
+ `docker run --rm -v "$PWD:/work" -w /work omninode run my-pipeline "audit the repo"`.
291
+
247
292
  ## Install
248
293
 
249
294
  ```bash
package/dist/cli/index.js CHANGED
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/version.ts
9
- var OMNINODE_VERSION = "0.9.0";
9
+ var OMNINODE_VERSION = "1.0.0";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -87,6 +87,7 @@ var agentConfigSchema = z.object({
87
87
  env: z.record(z.string()).optional(),
88
88
  input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
89
89
  timeout_ms: z.number().int().positive().optional(),
90
+ inherit_env: z.boolean().optional(),
90
91
  metadata: z.record(z.unknown()).optional()
91
92
  }).strict();
92
93
  var roleConfigSchema = z.object({
@@ -260,6 +261,7 @@ function toAgentConfig(raw) {
260
261
  env: raw.env,
261
262
  inputMode: raw.input_mode,
262
263
  timeoutMs: raw.timeout_ms,
264
+ inheritEnv: raw.inherit_env,
263
265
  metadata: raw.metadata
264
266
  };
265
267
  }
@@ -897,6 +899,17 @@ function registerModelsCommand(program) {
897
899
  import { spawn } from "child_process";
898
900
  var DEFAULT_TIMEOUT_MS2 = 6e5;
899
901
  var KILL_GRACE_MS = 5e3;
902
+ function buildChildEnv(config) {
903
+ const configured = config.env ?? {};
904
+ if (config.inheritEnv !== false) {
905
+ return { ...process.env, ...configured };
906
+ }
907
+ return {
908
+ PATH: process.env.PATH ?? "",
909
+ HOME: process.env.HOME ?? "",
910
+ ...configured
911
+ };
912
+ }
900
913
  function composeTaskText(input) {
901
914
  const sections = [`# Objective
902
915
  ${input.objective}`];
@@ -963,7 +976,7 @@ var ProcessAgent = class {
963
976
  [...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
964
977
  {
965
978
  cwd: this.config.cwd,
966
- env: { ...process.env, ...this.config.env },
979
+ env: buildChildEnv(this.config),
967
980
  stdio: ["pipe", "pipe", "pipe"]
968
981
  }
969
982
  );
@@ -1400,6 +1413,7 @@ var TaskEngine = class {
1400
1413
  };
1401
1414
  await this.options.store.save(task);
1402
1415
  this.log.info(`Created task ${task.id}.`);
1416
+ await this.audit("task.created", task.id, { objective: task.objective, project: task.project });
1403
1417
  return task;
1404
1418
  }
1405
1419
  async queue(id) {
@@ -1475,6 +1489,26 @@ var TaskEngine = class {
1475
1489
  }
1476
1490
  return this.transition(task, "cancelled");
1477
1491
  }
1492
+ /** Error recovery (§27 Phase 9): reset a failed/cancelled task so it can run again. */
1493
+ async retry(id) {
1494
+ const task = await this.mustGet(id);
1495
+ if (task.status !== "failed" && task.status !== "cancelled") {
1496
+ throw new TaskError(
1497
+ "TASK_INVALID",
1498
+ `Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1499
+ );
1500
+ }
1501
+ const { result: _discarded, ...rest } = task;
1502
+ const reset = {
1503
+ ...rest,
1504
+ status: "created",
1505
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1506
+ };
1507
+ await this.options.store.save(reset);
1508
+ await this.audit("task.retry", id, { from: task.status });
1509
+ this.log.info(`Task ${id} reset for retry.`);
1510
+ return reset;
1511
+ }
1478
1512
  async get(id) {
1479
1513
  return this.options.store.get(id);
1480
1514
  }
@@ -1500,16 +1534,68 @@ var TaskEngine = class {
1500
1534
  ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
1501
1535
  };
1502
1536
  await this.options.store.save(updated);
1537
+ await this.audit(`task.${to === "queued" ? "queued" : to}`, updated.id, {
1538
+ from: task.status
1539
+ });
1503
1540
  return updated;
1504
1541
  }
1542
+ async audit(action, id, detail) {
1543
+ await this.options.audit?.record({
1544
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1545
+ action,
1546
+ id,
1547
+ ...detail !== void 0 ? { detail } : {}
1548
+ });
1549
+ }
1505
1550
  };
1506
1551
  function generateTaskId() {
1507
1552
  return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1508
1553
  }
1509
1554
 
1510
- // src/memory/local.ts
1511
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1555
+ // src/audit/index.ts
1556
+ import { appendFile, mkdir as mkdir2 } from "fs/promises";
1512
1557
  import { dirname as dirname2 } from "path";
1558
+ var FileAuditLog = class {
1559
+ filePath;
1560
+ log;
1561
+ constructor(directory = `${process.cwd()}/.omninode`, log = logger) {
1562
+ this.filePath = `${directory}/audit.jsonl`;
1563
+ this.log = log.child({ component: "audit" });
1564
+ }
1565
+ async record(event) {
1566
+ try {
1567
+ await mkdir2(dirname2(this.filePath), { recursive: true });
1568
+ await appendFile(this.filePath, `${JSON.stringify(event)}
1569
+ `, "utf8");
1570
+ } catch (error) {
1571
+ this.log.warn(
1572
+ `Audit write failed: ${error instanceof Error ? error.message : String(error)}`
1573
+ );
1574
+ }
1575
+ }
1576
+ /** Recent events, oldest first; used by the CLI. */
1577
+ async recent(limit = 20) {
1578
+ const { readFile: readFile6 } = await import("fs/promises");
1579
+ let raw;
1580
+ try {
1581
+ raw = await readFile6(this.filePath, "utf8");
1582
+ } catch {
1583
+ return [];
1584
+ }
1585
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => {
1586
+ try {
1587
+ return JSON.parse(line);
1588
+ } catch {
1589
+ return void 0;
1590
+ }
1591
+ }).filter((event) => event !== void 0);
1592
+ return events.slice(-limit);
1593
+ }
1594
+ };
1595
+
1596
+ // src/memory/local.ts
1597
+ import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1598
+ import { dirname as dirname3 } from "path";
1513
1599
  var STOPWORDS = /* @__PURE__ */ new Set([
1514
1600
  "the",
1515
1601
  "and",
@@ -1622,7 +1708,7 @@ var LocalMemoryProvider = class {
1622
1708
  }
1623
1709
  async saveAll(entries) {
1624
1710
  const tmp = `${this.filePath}.tmp`;
1625
- await mkdir2(dirname2(this.filePath), { recursive: true });
1711
+ await mkdir3(dirname3(this.filePath), { recursive: true });
1626
1712
  await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
1627
1713
  `, "utf8");
1628
1714
  await rename2(tmp, this.filePath);
@@ -1829,6 +1915,7 @@ function createTaskEngine(config, log = logger, directory) {
1829
1915
  roles,
1830
1916
  store: new FileTaskStore(directory),
1831
1917
  ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
1918
+ audit: new FileAuditLog(),
1832
1919
  log
1833
1920
  });
1834
1921
  }
@@ -1894,6 +1981,17 @@ function registerTaskCommands(program) {
1894
1981
  const cancelled = await engine.cancel(taskId);
1895
1982
  console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
1896
1983
  });
1984
+ task.command("retry <task-id>").description("Reset a failed/cancelled task to created; combine with --run to execute it again.").option("--run", "Run the task again immediately after resetting it.").action(async (taskId, options) => {
1985
+ const config = loadConfig();
1986
+ const engine = createTaskEngine(config);
1987
+ const reset = await engine.retry(taskId);
1988
+ console.log(`Task ${reset.id} reset to "${reset.status}".`);
1989
+ if (!options.run) return;
1990
+ console.log(`Running task ${reset.id}...`);
1991
+ const finished = await engine.run(reset.id);
1992
+ printOutcome(finished);
1993
+ if (finished.status !== "completed") process.exitCode = 1;
1994
+ });
1897
1995
  }
1898
1996
  function printOutcome(task, options = {}) {
1899
1997
  console.log(` id: ${task.id}`);
@@ -2130,8 +2228,8 @@ var ReportService = class {
2130
2228
  };
2131
2229
 
2132
2230
  // src/reports/store.ts
2133
- import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
2134
- import { dirname as dirname3 } from "path";
2231
+ import { mkdir as mkdir4, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
2232
+ import { dirname as dirname4 } from "path";
2135
2233
  var FileReportStore = class {
2136
2234
  filePath;
2137
2235
  queue = Promise.resolve();
@@ -2204,7 +2302,7 @@ var FileReportStore = class {
2204
2302
  }
2205
2303
  async write(entries) {
2206
2304
  const tmp = `${this.filePath}.tmp`;
2207
- await mkdir3(dirname3(this.filePath), { recursive: true });
2305
+ await mkdir4(dirname4(this.filePath), { recursive: true });
2208
2306
  await writeFile3(tmp, `${JSON.stringify(entries, null, 2)}
2209
2307
  `, "utf8");
2210
2308
  await rename3(tmp, this.filePath);
@@ -2639,12 +2737,19 @@ var PipelineEngine = class {
2639
2737
  const run2 = {
2640
2738
  id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
2641
2739
  pipelineId: def.id,
2740
+ objective,
2642
2741
  status: "running",
2643
2742
  startedAt: now,
2644
2743
  stepRuns: []
2645
2744
  };
2646
2745
  await this.options.store.save(run2);
2647
2746
  this.log.info(`Running pipeline ${def.id} as ${run2.id}.`);
2747
+ await this.options.audit?.record({
2748
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2749
+ action: "pipeline.run.started",
2750
+ id: run2.id,
2751
+ detail: { pipeline: def.id, objective }
2752
+ });
2648
2753
  const deps = this.effectiveDependencies(def);
2649
2754
  const outcomes = /* @__PURE__ */ new Map();
2650
2755
  const pending = new Set(def.steps.map((s) => s.id));
@@ -2698,9 +2803,19 @@ ${contribution}`);
2698
2803
  const failed = [...outcomes.values()].some((o) => o.status === "failed");
2699
2804
  const finalStatus = failed ? "failed" : "completed";
2700
2805
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
2806
+ let resultSummary;
2807
+ for (let i = def.steps.length - 1; i >= 0; i -= 1) {
2808
+ const step = def.steps[i];
2809
+ const outcome = outcomes.get(step.id);
2810
+ if (outcome?.status !== "completed" || outcome.taskIds.length === 0) continue;
2811
+ const stepTasks = (await Promise.all(outcome.taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
2812
+ const summaries = stepTasks.map((task) => task.result?.summary).filter((summary) => summary !== void 0);
2813
+ if (summaries.length > 0) resultSummary = summaries.join("\n");
2814
+ break;
2815
+ }
2701
2816
  let combinedReportId;
2702
2817
  if (this.options.reports) {
2703
- const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
2818
+ const taskIds = def.steps.filter((step) => step.kind === "research" || step.kind === "analyze").flatMap((step) => outcomes.get(step.id)?.taskIds ?? []);
2704
2819
  const tasks = (await Promise.all(taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
2705
2820
  const collected = await this.options.reports.collectFromTasks(tasks);
2706
2821
  await this.options.reports.saveReports(collected);
@@ -2720,10 +2835,17 @@ ${contribution}`);
2720
2835
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
2721
2836
  ...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
2722
2837
  ...combinedReportId !== void 0 ? { combinedReportId } : {},
2723
- ...planId !== void 0 ? { planId } : {}
2838
+ ...planId !== void 0 ? { planId } : {},
2839
+ ...resultSummary !== void 0 ? { resultSummary } : {}
2724
2840
  };
2725
2841
  await this.options.store.save(finished);
2726
2842
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
2843
+ await this.options.audit?.record({
2844
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2845
+ action: "pipeline.run.finished",
2846
+ id: run2.id,
2847
+ detail: { pipeline: def.id, status: finalStatus }
2848
+ });
2727
2849
  return finished;
2728
2850
  }
2729
2851
  async get(runId) {
@@ -2868,6 +2990,12 @@ ${contribution}`);
2868
2990
  };
2869
2991
  }
2870
2992
  await this.options.plans?.save(plan);
2993
+ await this.options.audit?.record({
2994
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2995
+ action: "plan.generated",
2996
+ id: plan.id,
2997
+ detail: { generatedBy: plan.generatedBy, steps: plan.steps.length }
2998
+ });
2871
2999
  return {
2872
3000
  status: "completed",
2873
3001
  summary: plan.summary,
@@ -2925,8 +3053,8 @@ function createDefaultChatFn(providers) {
2925
3053
  }
2926
3054
 
2927
3055
  // src/pipelines/store.ts
2928
- import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
2929
- import { dirname as dirname4 } from "path";
3056
+ import { mkdir as mkdir5, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
3057
+ import { dirname as dirname5 } from "path";
2930
3058
  var FilePipelineRunStore = class {
2931
3059
  filePath;
2932
3060
  queue = Promise.resolve();
@@ -2973,7 +3101,7 @@ var FilePipelineRunStore = class {
2973
3101
  }
2974
3102
  async write(runs) {
2975
3103
  const tmp = `${this.filePath}.tmp`;
2976
- await mkdir4(dirname4(this.filePath), { recursive: true });
3104
+ await mkdir5(dirname5(this.filePath), { recursive: true });
2977
3105
  await writeFile4(tmp, `${JSON.stringify(runs, null, 2)}
2978
3106
  `, "utf8");
2979
3107
  await rename4(tmp, this.filePath);
@@ -2981,8 +3109,8 @@ var FilePipelineRunStore = class {
2981
3109
  };
2982
3110
 
2983
3111
  // src/planner/store.ts
2984
- import { mkdir as mkdir5, readFile as readFile5, rename as rename5, writeFile as writeFile5 } from "fs/promises";
2985
- import { dirname as dirname5 } from "path";
3112
+ import { mkdir as mkdir6, readFile as readFile5, rename as rename5, writeFile as writeFile5 } from "fs/promises";
3113
+ import { dirname as dirname6 } from "path";
2986
3114
  var FilePlanStore = class {
2987
3115
  filePath;
2988
3116
  queue = Promise.resolve();
@@ -3029,7 +3157,7 @@ var FilePlanStore = class {
3029
3157
  }
3030
3158
  async write(plans) {
3031
3159
  const tmp = `${this.filePath}.tmp`;
3032
- await mkdir5(dirname5(this.filePath), { recursive: true });
3160
+ await mkdir6(dirname6(this.filePath), { recursive: true });
3033
3161
  await writeFile5(tmp, `${JSON.stringify(plans, null, 2)}
3034
3162
  `, "utf8");
3035
3163
  await rename5(tmp, this.filePath);
@@ -3067,6 +3195,7 @@ function buildPipelineEngine(config, log = logger) {
3067
3195
  roles,
3068
3196
  store: new FileTaskStore(),
3069
3197
  ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
3198
+ audit: new FileAuditLog(),
3070
3199
  log
3071
3200
  });
3072
3201
  return new PipelineEngine({
@@ -3079,6 +3208,7 @@ function buildPipelineEngine(config, log = logger) {
3079
3208
  ...config.project.planner !== void 0 ? { planner: buildPlanner(config.project.planner, chat, log) } : {},
3080
3209
  plans: new FilePlanStore(),
3081
3210
  reports: new ReportService(new FileReportStore(), log),
3211
+ audit: new FileAuditLog(),
3082
3212
  log
3083
3213
  });
3084
3214
  }
@@ -3100,31 +3230,27 @@ function registerPipelineCommands(program) {
3100
3230
  });
3101
3231
  pipeline.command("run <id> [objective]").description(
3102
3232
  "Run a pipeline. The objective argument overrides the definition's default objective."
3103
- ).action(async (id, objective) => {
3233
+ ).action(async (id, objective) => runPipelineAction(id, objective));
3234
+ pipeline.command("retry <run-id> [objective]").description(
3235
+ "Error recovery: re-run a recorded pipeline run as a new run, reusing its objective."
3236
+ ).action(async (runId, objective) => {
3104
3237
  const config = loadConfig();
3105
- const def = findPipeline(config, id);
3106
3238
  const engine = buildPipelineEngine(config);
3107
- console.log(
3108
- `Running pipeline "${def.id}" (${def.steps.length} step(s))${objective ? ` \u2014 "${objective}"` : ""}...`
3109
- );
3110
- const finished = await engine.run(def, {
3111
- ...objective !== void 0 ? { objective } : {},
3112
- onStep: (stepRun) => {
3113
- const suffix = stepRun.error ? ` \u2014 ${stepRun.error}` : "";
3114
- console.log(` [${stepRun.stepId}] ${stepRun.status}${suffix}`);
3115
- }
3116
- });
3117
- if (finished.reports && finished.reports.length > 0) {
3118
- console.log(` reports: ${finished.reports.length} structured report(s)`);
3119
- }
3120
- if (finished.combinedReportId) {
3121
- console.log(` combined report: ${finished.combinedReportId}`);
3239
+ const previous = await engine.get(runId);
3240
+ if (!previous) {
3241
+ throw new PipelineError(
3242
+ "PIPELINE_NOT_FOUND",
3243
+ `Pipeline run "${runId}" does not exist in this project's run store.`
3244
+ );
3122
3245
  }
3123
- if (finished.planId) {
3124
- console.log(` plan: ${finished.planId}`);
3246
+ const def = config.project.pipelines.find((p) => p.id === previous.pipelineId);
3247
+ if (!def) {
3248
+ throw new PipelineError(
3249
+ "PIPELINE_NOT_FOUND",
3250
+ `Pipeline "${previous.pipelineId}" (run ${runId}) is no longer configured in omninode.yaml.`
3251
+ );
3125
3252
  }
3126
- console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
3127
- if (finished.status !== "completed") process.exitCode = 1;
3253
+ await runPipelineAction(def.id, objective ?? previous.objective);
3128
3254
  });
3129
3255
  pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
3130
3256
  const config = loadConfig();
@@ -3153,6 +3279,7 @@ function registerPipelineCommands(program) {
3153
3279
  console.log(` status: ${run2.status}`);
3154
3280
  console.log(` started: ${run2.startedAt ?? "?"}`);
3155
3281
  if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
3282
+ if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
3156
3283
  for (const stepRun of run2.stepRuns) {
3157
3284
  console.log(` [${stepRun.stepId}] ${stepRun.status}`);
3158
3285
  }
@@ -3161,6 +3288,38 @@ function registerPipelineCommands(program) {
3161
3288
  }
3162
3289
  });
3163
3290
  }
3291
+ async function runPipelineAction(id, objective) {
3292
+ const config = loadConfig();
3293
+ const def = findPipeline(config, id);
3294
+ const engine = buildPipelineEngine(config);
3295
+ console.log(
3296
+ `Running pipeline "${def.id}" (${def.steps.length} step(s))${objective ? ` \u2014 "${objective}"` : ""}...`
3297
+ );
3298
+ const finished = await engine.run(def, {
3299
+ ...objective !== void 0 ? { objective } : {},
3300
+ onStep: (stepRun) => {
3301
+ const suffix = stepRun.error ? ` \u2014 ${stepRun.error}` : "";
3302
+ console.log(` [${stepRun.stepId}] ${stepRun.status}${suffix}`);
3303
+ }
3304
+ });
3305
+ if (finished.reports && finished.reports.length > 0) {
3306
+ console.log(` reports: ${finished.reports.length} structured report(s)`);
3307
+ }
3308
+ if (finished.combinedReportId) {
3309
+ console.log(` combined report: ${finished.combinedReportId}`);
3310
+ }
3311
+ if (finished.planId) {
3312
+ console.log(` plan: ${finished.planId}`);
3313
+ }
3314
+ if (finished.resultSummary) {
3315
+ console.log(` result: ${finished.resultSummary}`);
3316
+ }
3317
+ console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
3318
+ if (finished.status !== "completed") process.exitCode = 1;
3319
+ }
3320
+ function registerRunAlias(program) {
3321
+ program.command("run <id> [objective]").description("Alias of `omninode pipeline run` \u2014 execute a configured pipeline end to end.").action(async (id, objective) => runPipelineAction(id, objective));
3322
+ }
3164
3323
  function findPipeline(config, id) {
3165
3324
  const def = config.project.pipelines.find((p) => p.id === id);
3166
3325
  if (!def) {
@@ -3272,6 +3431,96 @@ function registerPlanCommands(program) {
3272
3431
  });
3273
3432
  }
3274
3433
 
3434
+ // src/cli/commands/status.ts
3435
+ function groupBy(items, key) {
3436
+ const counts = {};
3437
+ for (const item of items) {
3438
+ const k = key(item);
3439
+ counts[k] = (counts[k] ?? 0) + 1;
3440
+ }
3441
+ return counts;
3442
+ }
3443
+ function countBy(items, key) {
3444
+ const counts = /* @__PURE__ */ new Map();
3445
+ for (const item of items) {
3446
+ const k = key(item);
3447
+ counts.set(k, (counts.get(k) ?? 0) + 1);
3448
+ }
3449
+ if (counts.size === 0) return "0";
3450
+ return `${items.length} (${[...counts.entries()].map(([k, v]) => `${k} ${v}`).join(", ")})`;
3451
+ }
3452
+ function registerStatusCommand(program) {
3453
+ program.command("status").description("Show the project's OmniNode state overview (config, tasks, runs, reports, plans, memory).").option("--json", "Emit machine-readable JSON (for scripts and CI).").action(async (options) => {
3454
+ const config = loadConfig();
3455
+ const tasks = await new FileTaskStore().list();
3456
+ const runs = await new FilePipelineRunStore().list();
3457
+ const reportStore = new FileReportStore();
3458
+ const reports = await reportStore.listReports();
3459
+ const combined = await reportStore.listCombined();
3460
+ const plans = await new FilePlanStore().list();
3461
+ let memory;
3462
+ if (config.project.memory !== void 0) {
3463
+ const service2 = new MemoryService(createMemoryProvider(config.project.memory));
3464
+ memory = {
3465
+ provider: service2.providerName,
3466
+ entries: (await service2.providerRef.query({ limit: 1e4 })).length
3467
+ };
3468
+ }
3469
+ if (options.json) {
3470
+ console.log(
3471
+ JSON.stringify(
3472
+ {
3473
+ version: OMNINODE_VERSION,
3474
+ project: config.project.name,
3475
+ providers: config.project.providers.map((p) => p.name),
3476
+ agents: config.project.agents.map((a) => a.name),
3477
+ roles: config.project.roles.map((r) => r.id),
3478
+ pipelines: config.project.pipelines.map((p) => p.id),
3479
+ tasks: { total: tasks.length, byStatus: groupBy(tasks, (t) => t.status) },
3480
+ runs: { total: runs.length, byStatus: groupBy(runs, (r) => r.status) },
3481
+ reports: { raw: reports.length, combined: combined.length },
3482
+ plans: plans.length,
3483
+ ...memory !== void 0 ? { memory } : {}
3484
+ },
3485
+ null,
3486
+ 2
3487
+ )
3488
+ );
3489
+ return;
3490
+ }
3491
+ console.log(`OmniNode v${OMNINODE_VERSION} \u2014 project "${config.project.name}"`);
3492
+ console.log(`providers: ${config.project.providers.map((p) => p.name).join(", ") || "none"}`);
3493
+ console.log(`agents: ${config.project.agents.map((a) => a.name).join(", ") || "none"}`);
3494
+ console.log(`roles: ${config.project.roles.map((r) => r.id).join(", ") || "none"}`);
3495
+ console.log(`pipelines: ${config.project.pipelines.map((p) => p.id).join(", ") || "none"}`);
3496
+ console.log(`tasks: ${countBy(tasks, (t) => t.status)}`);
3497
+ console.log(`runs: ${countBy(runs, (r) => r.status)}`);
3498
+ console.log(`reports: ${reports.length} raw, ${combined.length} combined`);
3499
+ console.log(`plans: ${plans.length}`);
3500
+ if (memory) {
3501
+ console.log(`memory: ${memory.provider}, ${memory.entries} entr(ies)`);
3502
+ } else {
3503
+ console.log("memory: not configured (local provider available \u2014 add a `memory:` section)");
3504
+ }
3505
+ });
3506
+ }
3507
+
3508
+ // src/cli/commands/audit.ts
3509
+ function registerAuditCommand(program) {
3510
+ program.command("audit").description("Show the recent audit log \u2014 task and pipeline lifecycle events (\xA724).").option("-n, --limit <count>", "Number of events to show (default 20).", (value) => Number(value), 20).action(async (options) => {
3511
+ const events = await new FileAuditLog().recent(options.limit);
3512
+ if (events.length === 0) {
3513
+ console.log("No audit events recorded yet.");
3514
+ return;
3515
+ }
3516
+ for (const event of events) {
3517
+ const id = event.id ? ` ${event.id}` : "";
3518
+ const detail = event.detail && Object.keys(event.detail).length > 0 ? ` ${JSON.stringify(event.detail)}` : "";
3519
+ console.log(`${event.at} ${event.action}${id}${detail}`);
3520
+ }
3521
+ });
3522
+ }
3523
+
3275
3524
  // src/cli/index.ts
3276
3525
  function createProgram() {
3277
3526
  const program = new Command();
@@ -3289,6 +3538,9 @@ function createProgram() {
3289
3538
  registerPipelineCommands(program);
3290
3539
  registerMemoryCommands(program);
3291
3540
  registerPlanCommands(program);
3541
+ registerStatusCommand(program);
3542
+ registerAuditCommand(program);
3543
+ registerRunAlias(program);
3292
3544
  return program;
3293
3545
  }
3294
3546
  async function run(argv) {