@hilbras/omninode 0.9.0 → 0.10.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,9 @@ 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
+ **v0.10.0 — Phase 9: End-to-End Workflow** (see [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.md)
22
+ for the full plan and roadmap). The full loop from §28 works in one command:
23
+ `User → project context → memory → research agents → reports → aggregation → planner → plan → execution → result`.
23
24
 
24
25
  What exists today:
25
26
 
@@ -97,6 +98,15 @@ What exists today:
97
98
  - Plans persist to `.omninode/plans.json`; pipeline runs record `planId`
98
99
  - Configure with `planner: { kind: model | heuristic, model, instruction }`
99
100
  — never hardcoded to any provider
101
+ - **End-to-end workflow** (§28, §27 Phase 9):
102
+ - One-command execution via the top-level `run` alias (`omninode run <pipeline> "<objective>"`)
103
+ - `omninode status` — full project state overview: providers, agents,
104
+ roles, pipelines, task/run/report/plan counts, memory provider + entries
105
+ - Pipeline runs persist their objective and a final `resultSummary` (§28 "Result")
106
+ - Error recovery: `task retry [--run]` (reset a failed/cancelled task) and
107
+ `pipeline retry <run-id>` (re-run a recorded failed run as a new run)
108
+ - Full integration test (`tests/e2e.test.ts`) drives the entire §28 workflow
109
+ through the real CLI against a local gateway
100
110
  - Typed error hierarchy (`src/errors/`)
101
111
  - Leveled logger with pluggable sink (`src/logger/`)
102
112
  - Configuration system: `omninode.yaml` with `${ENV_VAR}` expansion and strict
@@ -190,6 +200,14 @@ project:
190
200
  model: my-gateway:chatgpt
191
201
  ```
192
202
 
203
+ ```bash
204
+ omninode init
205
+ omninode status
206
+ omninode run repo-audit "Audit the authentication module" # the whole workflow
207
+ omninode pipeline retry run-xxxx # recover a failed run
208
+ omninode task retry task-yyyy --run # recover a failed task
209
+ ```
210
+
193
211
  Persistent memory (§19–§20) — opt in with `memory: { provider: local }` or
194
212
  `provider: remembera` in `omninode.yaml`:
195
213
 
@@ -236,8 +254,9 @@ Phases 0–11 are laid out in [docs/DEVELOPMENT_PLAN.md](docs/DEVELOPMENT_PLAN.m
236
254
  | v0.7.0 | Multi-AI Reports |
237
255
  | v0.7.0 | Multi-AI Reports |
238
256
  | v0.8.0 | Remembera / Memory |
239
- | **v0.9.0** | **Planner (current)** |
240
- | v0.10.0 | End-to-End Runtime |
257
+ | v0.8.0 | Remembera / Memory |
258
+ | v0.9.0 | Planner |
259
+ | **v0.10.0** | **End-to-End Workflow (current)** |
241
260
  | v1.0.0 | Production Release |
242
261
 
243
262
  Core architectural rule: OmniNode must run successfully with **zero Hilbras
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 = "0.10.0";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -1475,6 +1475,25 @@ var TaskEngine = class {
1475
1475
  }
1476
1476
  return this.transition(task, "cancelled");
1477
1477
  }
1478
+ /** Error recovery (§27 Phase 9): reset a failed/cancelled task so it can run again. */
1479
+ async retry(id) {
1480
+ const task = await this.mustGet(id);
1481
+ if (task.status !== "failed" && task.status !== "cancelled") {
1482
+ throw new TaskError(
1483
+ "TASK_INVALID",
1484
+ `Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1485
+ );
1486
+ }
1487
+ const { result: _discarded, ...rest } = task;
1488
+ const reset = {
1489
+ ...rest,
1490
+ status: "created",
1491
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1492
+ };
1493
+ await this.options.store.save(reset);
1494
+ this.log.info(`Task ${id} reset for retry.`);
1495
+ return reset;
1496
+ }
1478
1497
  async get(id) {
1479
1498
  return this.options.store.get(id);
1480
1499
  }
@@ -1894,6 +1913,17 @@ function registerTaskCommands(program) {
1894
1913
  const cancelled = await engine.cancel(taskId);
1895
1914
  console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
1896
1915
  });
1916
+ 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) => {
1917
+ const config = loadConfig();
1918
+ const engine = createTaskEngine(config);
1919
+ const reset = await engine.retry(taskId);
1920
+ console.log(`Task ${reset.id} reset to "${reset.status}".`);
1921
+ if (!options.run) return;
1922
+ console.log(`Running task ${reset.id}...`);
1923
+ const finished = await engine.run(reset.id);
1924
+ printOutcome(finished);
1925
+ if (finished.status !== "completed") process.exitCode = 1;
1926
+ });
1897
1927
  }
1898
1928
  function printOutcome(task, options = {}) {
1899
1929
  console.log(` id: ${task.id}`);
@@ -2639,6 +2669,7 @@ var PipelineEngine = class {
2639
2669
  const run2 = {
2640
2670
  id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
2641
2671
  pipelineId: def.id,
2672
+ objective,
2642
2673
  status: "running",
2643
2674
  startedAt: now,
2644
2675
  stepRuns: []
@@ -2698,9 +2729,19 @@ ${contribution}`);
2698
2729
  const failed = [...outcomes.values()].some((o) => o.status === "failed");
2699
2730
  const finalStatus = failed ? "failed" : "completed";
2700
2731
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
2732
+ let resultSummary;
2733
+ for (let i = def.steps.length - 1; i >= 0; i -= 1) {
2734
+ const step = def.steps[i];
2735
+ const outcome = outcomes.get(step.id);
2736
+ if (outcome?.status !== "completed" || outcome.taskIds.length === 0) continue;
2737
+ const stepTasks = (await Promise.all(outcome.taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
2738
+ const summaries = stepTasks.map((task) => task.result?.summary).filter((summary) => summary !== void 0);
2739
+ if (summaries.length > 0) resultSummary = summaries.join("\n");
2740
+ break;
2741
+ }
2701
2742
  let combinedReportId;
2702
2743
  if (this.options.reports) {
2703
- const taskIds = [...outcomes.values()].flatMap((o) => o.taskIds);
2744
+ const taskIds = def.steps.filter((step) => step.kind === "research" || step.kind === "analyze").flatMap((step) => outcomes.get(step.id)?.taskIds ?? []);
2704
2745
  const tasks = (await Promise.all(taskIds.map((id) => this.options.tasks.get(id)))).filter((task) => task !== void 0);
2705
2746
  const collected = await this.options.reports.collectFromTasks(tasks);
2706
2747
  await this.options.reports.saveReports(collected);
@@ -2720,7 +2761,8 @@ ${contribution}`);
2720
2761
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
2721
2762
  ...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
2722
2763
  ...combinedReportId !== void 0 ? { combinedReportId } : {},
2723
- ...planId !== void 0 ? { planId } : {}
2764
+ ...planId !== void 0 ? { planId } : {},
2765
+ ...resultSummary !== void 0 ? { resultSummary } : {}
2724
2766
  };
2725
2767
  await this.options.store.save(finished);
2726
2768
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
@@ -3100,31 +3142,27 @@ function registerPipelineCommands(program) {
3100
3142
  });
3101
3143
  pipeline.command("run <id> [objective]").description(
3102
3144
  "Run a pipeline. The objective argument overrides the definition's default objective."
3103
- ).action(async (id, objective) => {
3145
+ ).action(async (id, objective) => runPipelineAction(id, objective));
3146
+ pipeline.command("retry <run-id> [objective]").description(
3147
+ "Error recovery: re-run a recorded pipeline run as a new run, reusing its objective."
3148
+ ).action(async (runId, objective) => {
3104
3149
  const config = loadConfig();
3105
- const def = findPipeline(config, id);
3106
3150
  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}`);
3151
+ const previous = await engine.get(runId);
3152
+ if (!previous) {
3153
+ throw new PipelineError(
3154
+ "PIPELINE_NOT_FOUND",
3155
+ `Pipeline run "${runId}" does not exist in this project's run store.`
3156
+ );
3122
3157
  }
3123
- if (finished.planId) {
3124
- console.log(` plan: ${finished.planId}`);
3158
+ const def = config.project.pipelines.find((p) => p.id === previous.pipelineId);
3159
+ if (!def) {
3160
+ throw new PipelineError(
3161
+ "PIPELINE_NOT_FOUND",
3162
+ `Pipeline "${previous.pipelineId}" (run ${runId}) is no longer configured in omninode.yaml.`
3163
+ );
3125
3164
  }
3126
- console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
3127
- if (finished.status !== "completed") process.exitCode = 1;
3165
+ await runPipelineAction(def.id, objective ?? previous.objective);
3128
3166
  });
3129
3167
  pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
3130
3168
  const config = loadConfig();
@@ -3153,6 +3191,7 @@ function registerPipelineCommands(program) {
3153
3191
  console.log(` status: ${run2.status}`);
3154
3192
  console.log(` started: ${run2.startedAt ?? "?"}`);
3155
3193
  if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
3194
+ if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
3156
3195
  for (const stepRun of run2.stepRuns) {
3157
3196
  console.log(` [${stepRun.stepId}] ${stepRun.status}`);
3158
3197
  }
@@ -3161,6 +3200,38 @@ function registerPipelineCommands(program) {
3161
3200
  }
3162
3201
  });
3163
3202
  }
3203
+ async function runPipelineAction(id, objective) {
3204
+ const config = loadConfig();
3205
+ const def = findPipeline(config, id);
3206
+ const engine = buildPipelineEngine(config);
3207
+ console.log(
3208
+ `Running pipeline "${def.id}" (${def.steps.length} step(s))${objective ? ` \u2014 "${objective}"` : ""}...`
3209
+ );
3210
+ const finished = await engine.run(def, {
3211
+ ...objective !== void 0 ? { objective } : {},
3212
+ onStep: (stepRun) => {
3213
+ const suffix = stepRun.error ? ` \u2014 ${stepRun.error}` : "";
3214
+ console.log(` [${stepRun.stepId}] ${stepRun.status}${suffix}`);
3215
+ }
3216
+ });
3217
+ if (finished.reports && finished.reports.length > 0) {
3218
+ console.log(` reports: ${finished.reports.length} structured report(s)`);
3219
+ }
3220
+ if (finished.combinedReportId) {
3221
+ console.log(` combined report: ${finished.combinedReportId}`);
3222
+ }
3223
+ if (finished.planId) {
3224
+ console.log(` plan: ${finished.planId}`);
3225
+ }
3226
+ if (finished.resultSummary) {
3227
+ console.log(` result: ${finished.resultSummary}`);
3228
+ }
3229
+ console.log(`Pipeline ${finished.status}. Run id: ${finished.id}`);
3230
+ if (finished.status !== "completed") process.exitCode = 1;
3231
+ }
3232
+ function registerRunAlias(program) {
3233
+ 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));
3234
+ }
3164
3235
  function findPipeline(config, id) {
3165
3236
  const def = config.project.pipelines.find((p) => p.id === id);
3166
3237
  if (!def) {
@@ -3272,6 +3343,52 @@ function registerPlanCommands(program) {
3272
3343
  });
3273
3344
  }
3274
3345
 
3346
+ // src/cli/commands/status.ts
3347
+ function countBy(items, key) {
3348
+ const counts = /* @__PURE__ */ new Map();
3349
+ for (const item of items) {
3350
+ const k = key(item);
3351
+ counts.set(k, (counts.get(k) ?? 0) + 1);
3352
+ }
3353
+ if (counts.size === 0) return "0";
3354
+ return `${items.length} (${[...counts.entries()].map(([k, v]) => `${k} ${v}`).join(", ")})`;
3355
+ }
3356
+ function registerStatusCommand(program) {
3357
+ program.command("status").description("Show the project's OmniNode state overview (config, tasks, runs, reports, plans, memory).").action(async () => {
3358
+ const config = loadConfig();
3359
+ console.log(`OmniNode v${OMNINODE_VERSION} \u2014 project "${config.project.name}"`);
3360
+ console.log(
3361
+ `providers: ${config.project.providers.length > 0 ? config.project.providers.map((p) => p.name).join(", ") : "none"}`
3362
+ );
3363
+ console.log(
3364
+ `agents: ${config.project.agents.length > 0 ? config.project.agents.map((a) => a.name).join(", ") : "none"}`
3365
+ );
3366
+ console.log(
3367
+ `roles: ${config.project.roles.length > 0 ? config.project.roles.map((r) => r.id).join(", ") : "none"}`
3368
+ );
3369
+ console.log(
3370
+ `pipelines: ${config.project.pipelines.length > 0 ? config.project.pipelines.map((p) => p.id).join(", ") : "none"}`
3371
+ );
3372
+ const tasks = await new FileTaskStore().list();
3373
+ console.log(`tasks: ${countBy(tasks, (t) => t.status)}`);
3374
+ const runs = await new FilePipelineRunStore().list();
3375
+ console.log(`runs: ${countBy(runs, (r) => r.status)}`);
3376
+ const reportStore = new FileReportStore();
3377
+ const reports = await reportStore.listReports();
3378
+ const combined = await reportStore.listCombined();
3379
+ console.log(`reports: ${reports.length} raw, ${combined.length} combined`);
3380
+ const plans = await new FilePlanStore().list();
3381
+ console.log(`plans: ${plans.length}`);
3382
+ if (config.project.memory !== void 0) {
3383
+ const memory = new MemoryService(createMemoryProvider(config.project.memory));
3384
+ const entries = await memory.providerRef.query({ limit: 1e4 });
3385
+ console.log(`memory: ${memory.providerName}, ${entries.length} entr(ies)`);
3386
+ } else {
3387
+ console.log("memory: not configured (local provider available \u2014 add a `memory:` section)");
3388
+ }
3389
+ });
3390
+ }
3391
+
3275
3392
  // src/cli/index.ts
3276
3393
  function createProgram() {
3277
3394
  const program = new Command();
@@ -3289,6 +3406,8 @@ function createProgram() {
3289
3406
  registerPipelineCommands(program);
3290
3407
  registerMemoryCommands(program);
3291
3408
  registerPlanCommands(program);
3409
+ registerStatusCommand(program);
3410
+ registerRunAlias(program);
3292
3411
  return program;
3293
3412
  }
3294
3413
  async function run(argv) {