@hilbras/omninode 2.0.0-alpha.10 → 2.0.0-alpha.11

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,7 +18,7 @@ existing intelligence; it does not try to become another model.
18
18
 
19
19
  ## Status
20
20
 
21
- **v2.0.0-alpha.10 — v2 Phase 10: Aggregation Improvements**
21
+ **v2.0.0-alpha.11 — v2 Phase 11: Planner v2**
22
22
  (the reliability & interoperability line: architecture → execution →
23
23
  protocol → adapters → pipelines → providers → OmniHilbras → memory → reports) (the v2
24
24
  roadmap is [docs/ROADMAP_V2.md](docs/ROADMAP_V2.md); the v1 plan is complete:
@@ -31,6 +31,13 @@ for the plan and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design).
31
31
 
32
32
  ### What's new in v2 (so far)
33
33
 
34
+ - **Phase 11 — Planner v2**: the planner now receives the full §15 input —
35
+ task, project context, agent reports, **aggregated findings (consensus and
36
+ conflicts included)**, memory and **constraints** (settable per pipeline
37
+ step). Plans carry an explicit goal, every generated plan is **schema
38
+ validated**, invalid output produces a **structured error** or falls back to
39
+ heuristic planning, and a guard test proves the planner module depends on no
40
+ specific provider.
34
41
  - **Phase 10 — Aggregation Improvements**: when agents disagree,
35
42
  OmniNode now says so instead of flattening the answer — **conflict
36
43
  detection** preserves every position (severity spread, divergent
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 = "2.0.0-alpha.10";
9
+ var OMNINODE_VERSION = "2.0.0-alpha.11";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -109,7 +109,9 @@ var pipelineStepConfigSchema = z.object({
109
109
  model: z.string().optional(),
110
110
  depends_on: z.array(z.string()).optional(),
111
111
  retries: z.number().int().min(0).max(10).optional(),
112
- condition: z.enum(["always", "on-success", "on-failure"]).optional()
112
+ condition: z.enum(["always", "on-success", "on-failure"]).optional(),
113
+ /** Constraints passed to the planner for plan steps (§15). */
114
+ constraints: z.array(z.string()).optional()
113
115
  }).strict();
114
116
  var pipelineConfigSchema = z.object({
115
117
  id: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "pipeline id must be kebab-case"),
@@ -239,7 +241,8 @@ function toPipelineDefinition(raw) {
239
241
  model: step.model,
240
242
  dependsOn: step.depends_on,
241
243
  retries: step.retries,
242
- condition: step.condition
244
+ condition: step.condition,
245
+ constraints: step.constraints
243
246
  }))
244
247
  };
245
248
  }
@@ -3532,19 +3535,32 @@ var HeuristicPlanner = class {
3532
3535
  name = "heuristic";
3533
3536
  async plan(request) {
3534
3537
  const steps = [];
3535
- const findings = request.reports.flatMap(
3536
- (report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
3537
- );
3538
3538
  const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
3539
- for (const [index, text] of recommendations.entries()) {
3539
+ for (const text of recommendations) {
3540
3540
  steps.push({
3541
3541
  id: `step-${steps.length + 1}`,
3542
3542
  title: truncate5(text, 120),
3543
3543
  order: steps.length + 1,
3544
- ...index > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
3544
+ ...steps.length > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
3545
3545
  });
3546
3546
  }
3547
- const significant = findings.filter(({ finding }) => finding.severity ? SEVERITY_RANK2[finding.severity] >= SEVERITY_RANK2.high : false).sort((a, b) => SEVERITY_RANK2[b.finding.severity ?? "info"] - SEVERITY_RANK2[a.finding.severity ?? "info"]);
3547
+ const contributed = request.aggregated ? request.aggregated.findings.map((finding) => ({
3548
+ finding: {
3549
+ id: `agg-${slug(finding.title)}`,
3550
+ title: finding.title,
3551
+ detail: finding.details[0] ?? finding.title,
3552
+ ...finding.severity ? { severity: finding.severity } : {},
3553
+ ...finding.recommendations && finding.recommendations.length > 0 ? { recommendation: finding.recommendations[0] } : {}
3554
+ },
3555
+ agent: finding.sources.map((s) => s.agent).join("+")
3556
+ })) : request.reports.flatMap(
3557
+ (report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
3558
+ );
3559
+ const significant = contributed.filter(
3560
+ ({ finding }) => finding.severity ? SEVERITY_RANK2[finding.severity] >= SEVERITY_RANK2.high : false
3561
+ ).sort(
3562
+ (a, b) => SEVERITY_RANK2[b.finding.severity ?? "info"] - SEVERITY_RANK2[a.finding.severity ?? "info"]
3563
+ );
3548
3564
  for (const { finding } of significant) {
3549
3565
  steps.push(remediationStep(finding, steps.length + 1));
3550
3566
  }
@@ -3557,12 +3573,20 @@ var HeuristicPlanner = class {
3557
3573
  });
3558
3574
  }
3559
3575
  const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
3576
+ const conflicts = request.aggregated?.conflicts ?? [];
3577
+ if (conflicts.length > 0) {
3578
+ risks.push(
3579
+ `${conflicts.length} unresolved agent disagreement(s): ${conflicts.map((c) => c.findingTitle).join("; ")}`
3580
+ );
3581
+ }
3560
3582
  return {
3561
3583
  id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
3562
3584
  objective: request.objective,
3585
+ goal: request.aggregated?.title ?? truncate5(request.objective, 120),
3563
3586
  summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
3564
3587
  steps,
3565
3588
  ...risks.length > 0 ? { risks } : {},
3589
+ ...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
3566
3590
  generatedBy: "heuristic",
3567
3591
  ...request.taskId !== void 0 ? { taskId: request.taskId } : {},
3568
3592
  ...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
@@ -3577,24 +3601,76 @@ function remediationStep(finding, order) {
3577
3601
  description: finding.detail,
3578
3602
  order,
3579
3603
  dependsOn: order > 1 ? [`step-${order - 1}`] : [],
3580
- sourceFindings: [finding.id]
3604
+ sourceFindings: [finding.id],
3605
+ ...finding.recommendation ? { recommendation: finding.recommendation } : {}
3581
3606
  };
3582
3607
  }
3583
3608
  function dedupe(texts) {
3584
3609
  return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
3585
3610
  }
3611
+ function slug(text) {
3612
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
3613
+ }
3586
3614
  function truncate5(text, max) {
3587
3615
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
3588
3616
  }
3589
3617
 
3590
3618
  // src/planner/model.ts
3591
3619
  import { randomBytes as randomBytes4 } from "crypto";
3620
+
3621
+ // src/planner/schema.ts
3592
3622
  import { z as z3 } from "zod";
3623
+ var planStepSchema = z3.object({
3624
+ id: z3.string().min(1).optional(),
3625
+ title: z3.string().min(1),
3626
+ description: z3.string().optional(),
3627
+ order: z3.number().int().optional(),
3628
+ targets: z3.array(z3.string()).optional(),
3629
+ acceptance_criteria: z3.array(z3.string()).optional(),
3630
+ depends_on: z3.array(z3.string()).optional(),
3631
+ source_findings: z3.array(z3.string()).optional()
3632
+ });
3633
+ var planJsonSchema = z3.object({
3634
+ goal: z3.string().min(1),
3635
+ summary: z3.string().min(1),
3636
+ steps: z3.array(planStepSchema).min(1),
3637
+ risks: z3.array(z3.string()).optional(),
3638
+ notes: z3.array(z3.string()).optional()
3639
+ });
3640
+ function validatePlan(plan) {
3641
+ const issues = [];
3642
+ const stepIds = /* @__PURE__ */ new Set();
3643
+ for (const [index, step] of plan.steps.entries()) {
3644
+ if (stepIds.has(step.id)) {
3645
+ issues.push({ path: `steps.${index}.id`, message: `duplicate step id "${step.id}"` });
3646
+ }
3647
+ stepIds.add(step.id);
3648
+ if (!step.title || step.title.trim().length === 0) {
3649
+ issues.push({ path: `steps.${index}.title`, message: "title must not be empty" });
3650
+ }
3651
+ for (const dep of step.dependsOn ?? []) {
3652
+ if (!stepIds.has(dep) && !plan.steps.some((s) => s.id === dep)) {
3653
+ issues.push({ path: `steps.${index}.dependsOn`, message: `unknown step dependency "${dep}"` });
3654
+ }
3655
+ }
3656
+ }
3657
+ if (plan.steps.length === 0) {
3658
+ issues.push({ path: "steps", message: "a plan needs at least one step" });
3659
+ }
3660
+ if (!plan.summary || plan.summary.trim().length === 0) {
3661
+ issues.push({ path: "summary", message: "summary must not be empty" });
3662
+ }
3663
+ return issues.length > 0 ? { valid: false, issues, reason: `plan failed validation (${issues.length} issue(s))` } : { valid: true, plan };
3664
+ }
3593
3665
 
3594
3666
  // src/planner/context.ts
3595
3667
  function buildPlannerContext(request) {
3596
- const parts = [`# Objective
3668
+ const parts = [`# Task
3597
3669
  ${request.objective}`];
3670
+ if (request.projectContext) {
3671
+ parts.push(`# Project context
3672
+ ${request.projectContext}`);
3673
+ }
3598
3674
  if (request.role) {
3599
3675
  const roleLines = [`# Role
3600
3676
  ${request.role.name} (${request.role.id})`];
@@ -3614,7 +3690,7 @@ ${request.role.name} (${request.role.id})`];
3614
3690
  )
3615
3691
  );
3616
3692
  parts.push(
3617
- `# Findings (${request.reports.length} report(s) by ${agents})
3693
+ `# Agent reports (${request.reports.length} by ${agents})
3618
3694
  ` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
3619
3695
  );
3620
3696
  const recommendationLines = request.reports.flatMap(
@@ -3624,9 +3700,20 @@ ${request.role.name} (${request.role.id})`];
3624
3700
  parts.push(`# Recommendations
3625
3701
  ${recommendationLines.join("\n")}`);
3626
3702
  }
3627
- const summaryLines = request.reports.map((report) => `- [${report.agent}] ${report.summary}`);
3628
- parts.push(`# Report summaries
3629
- ${summaryLines.join("\n")}`);
3703
+ }
3704
+ if (request.aggregated) {
3705
+ const aggregatedLines = [
3706
+ `- findings: ${request.aggregated.findings.length} (agreements: ${request.aggregated.agreements.length}, conflicts: ${request.aggregated.conflicts.length})`,
3707
+ ...request.aggregated.findings.map(
3708
+ (finding) => `- [${finding.severity ?? "info"}] ${finding.title} (x${finding.occurrences})`
3709
+ ),
3710
+ // Conflicts stay visible to the planner — it must plan around disagreement.
3711
+ ...request.aggregated.conflicts.map(
3712
+ (conflict) => `- CONFLICT (${conflict.type}) on "${conflict.findingTitle}": ${conflict.note}`
3713
+ )
3714
+ ];
3715
+ parts.push(`# Aggregated findings
3716
+ ${aggregatedLines.join("\n")}`);
3630
3717
  }
3631
3718
  if (request.memory && request.memory.length > 0) {
3632
3719
  parts.push(
@@ -3634,7 +3721,11 @@ ${summaryLines.join("\n")}`);
3634
3721
  ` + request.memory.map((entry2) => `- [${entry2.scope}] ${entry2.content}`).join("\n")
3635
3722
  );
3636
3723
  }
3637
- if (request.context && request.context.length > 0) {
3724
+ if (request.constraints && request.constraints.length > 0) {
3725
+ parts.push(`# Constraints
3726
+ ${request.constraints.map((c) => `- ${c}`).join("\n")}`);
3727
+ }
3728
+ if (request.context) {
3638
3729
  parts.push(`# Research context
3639
3730
  ${request.context}`);
3640
3731
  }
@@ -3642,22 +3733,6 @@ ${request.context}`);
3642
3733
  }
3643
3734
 
3644
3735
  // src/planner/model.ts
3645
- var planStepJsonSchema = z3.object({
3646
- id: z3.string().optional(),
3647
- title: z3.string().min(1),
3648
- description: z3.string().optional(),
3649
- order: z3.number().int().optional(),
3650
- targets: z3.array(z3.string()).optional(),
3651
- acceptance_criteria: z3.array(z3.string()).optional(),
3652
- depends_on: z3.array(z3.string()).optional(),
3653
- source_findings: z3.array(z3.string()).optional()
3654
- }).strict();
3655
- var planJsonSchema = z3.object({
3656
- summary: z3.string().min(1),
3657
- steps: z3.array(planStepJsonSchema).min(1),
3658
- risks: z3.array(z3.string()).optional(),
3659
- notes: z3.array(z3.string()).optional()
3660
- }).strict();
3661
3736
  var ModelPlanner = class {
3662
3737
  constructor(options) {
3663
3738
  this.options = options;
@@ -3671,7 +3746,7 @@ var ModelPlanner = class {
3671
3746
  const messages = [
3672
3747
  {
3673
3748
  role: "system",
3674
- content: 'You are the OmniNode planner. Given the objective and the combined research context, produce a final analysis and a prioritized, actionable implementation plan. Respond with JSON ONLY (no prose, no code fences) matching exactly this shape: {"summary": string, "steps": [{"id": string, "title": string, "description": string, "order": number, "targets": string[], "acceptance_criteria": string[], "source_findings": string[]}], "risks": string[], "notes": string[]}. ' + (this.options.instruction ?? "")
3749
+ content: 'You are the OmniNode planner. Given the objective and the combined research context, produce a final analysis and a prioritized, actionable implementation plan. Respond with JSON ONLY (no prose, no code fences) matching exactly this shape: {"goal": string, "summary": string, "steps": [{"id": string, "title": string, "description": string, "order": number, "targets": string[], "acceptance_criteria": string[], "source_findings": string[]}], "risks": string[], "notes": string[]}. ' + (this.options.instruction ?? "")
3675
3750
  },
3676
3751
  { role: "user", content: context }
3677
3752
  ];
@@ -3682,60 +3757,86 @@ var ModelPlanner = class {
3682
3757
  return this.degrade(request, `planner model call failed: ${describe(error)}`);
3683
3758
  }
3684
3759
  const parsed = parsePlanJson(responseText);
3685
- if (!parsed) {
3686
- return this.degrade(request, "planner model output was not valid plan JSON");
3760
+ if (!parsed.ok) {
3761
+ return this.degrade(request, `planner model output rejected: ${parsed.reason}`, parsed.issues);
3687
3762
  }
3688
3763
  const plan = {
3689
3764
  id: `plan-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
3690
3765
  objective: request.objective,
3766
+ goal: parsed.goal,
3691
3767
  summary: parsed.summary,
3692
3768
  steps: parsed.steps,
3693
3769
  ...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
3694
3770
  ...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
3771
+ ...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
3695
3772
  generatedBy: `model:${this.options.model}`,
3696
3773
  model: this.options.model,
3697
3774
  ...request.taskId !== void 0 ? { taskId: request.taskId } : {},
3698
3775
  ...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
3699
3776
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3700
3777
  };
3701
- return plan;
3778
+ const validation = validatePlan(plan);
3779
+ if (!validation.valid) {
3780
+ return this.degrade(request, `generated plan rejected: ${validation.reason}`, validation.issues);
3781
+ }
3782
+ return validation.plan;
3702
3783
  }
3703
- async degrade(request, reason) {
3784
+ async degrade(request, reason, issues = []) {
3704
3785
  if (this.options.fallback) {
3705
3786
  this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
3706
3787
  return this.options.fallback.plan(request);
3707
3788
  }
3708
- throw new PlannerError("PLANNER_FAILED", `Planner failed: ${reason}.`);
3789
+ throw new PlannerError("PLAN_INVALID", `Planner failed: ${reason}.`, {
3790
+ details: { reason, issues }
3791
+ });
3709
3792
  }
3710
3793
  };
3711
3794
  function parsePlanJson(text) {
3712
3795
  const unfenced = text.replace(/```(?:json)?/gi, "");
3713
3796
  const start = unfenced.indexOf("{");
3714
3797
  const end = unfenced.lastIndexOf("}");
3715
- if (start < 0 || end <= start) return void 0;
3798
+ if (start < 0 || end <= start) {
3799
+ return { ok: false, reason: "planner output contained no JSON object", issues: [] };
3800
+ }
3801
+ let json;
3716
3802
  try {
3717
- const json = JSON.parse(unfenced.slice(start, end + 1));
3718
- const parsed = planJsonSchema.safeParse(json);
3719
- if (!parsed.success) return void 0;
3720
- const steps = parsed.data.steps.map((step, index) => ({
3721
- id: step.id ?? `step-${index + 1}`,
3722
- title: step.title,
3723
- description: step.description,
3724
- order: step.order ?? index + 1,
3725
- targets: step.targets,
3726
- acceptanceCriteria: step.acceptance_criteria,
3727
- dependsOn: step.depends_on,
3728
- sourceFindings: step.source_findings
3729
- }));
3803
+ json = JSON.parse(unfenced.slice(start, end + 1));
3804
+ } catch (error) {
3730
3805
  return {
3731
- summary: parsed.data.summary,
3732
- steps: steps.sort((a, b) => a.order - b.order),
3733
- risks: parsed.data.risks,
3734
- notes: parsed.data.notes
3806
+ ok: false,
3807
+ reason: `planner output was not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
3808
+ issues: []
3735
3809
  };
3736
- } catch {
3737
- return void 0;
3738
3810
  }
3811
+ const parsed = planJsonSchema.safeParse(json);
3812
+ if (!parsed.success) {
3813
+ return {
3814
+ ok: false,
3815
+ reason: "planner output did not match the plan schema",
3816
+ issues: parsed.error.issues.map((issue) => ({
3817
+ path: issue.path.join(".") || "(root)",
3818
+ message: issue.message
3819
+ }))
3820
+ };
3821
+ }
3822
+ const steps = parsed.data.steps.map((step, index) => ({
3823
+ id: step.id ?? `step-${index + 1}`,
3824
+ title: step.title,
3825
+ description: step.description,
3826
+ order: step.order ?? index + 1,
3827
+ targets: step.targets,
3828
+ acceptanceCriteria: step.acceptance_criteria,
3829
+ dependsOn: step.depends_on,
3830
+ sourceFindings: step.source_findings
3831
+ })).sort((a, b) => a.order - b.order);
3832
+ return {
3833
+ ok: true,
3834
+ goal: parsed.data.goal,
3835
+ summary: parsed.data.summary,
3836
+ steps,
3837
+ risks: parsed.data.risks,
3838
+ notes: parsed.data.notes
3839
+ };
3739
3840
  }
3740
3841
  function describe(error) {
3741
3842
  return error instanceof Error ? error.message : String(error);
@@ -4173,11 +4274,14 @@ ${contribution}`);
4173
4274
  };
4174
4275
  }
4175
4276
  async runPlan(step, ctx) {
4277
+ const aggregated = ctx.reports.length > 0 ? aggregateReports(ctx.reports) : void 0;
4176
4278
  const request = {
4177
4279
  objective: ctx.objective,
4178
4280
  reports: ctx.reports,
4281
+ ...aggregated !== void 0 ? { aggregated } : {},
4179
4282
  ...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
4180
4283
  ...ctx.role ? { role: ctx.role } : {},
4284
+ ...step.constraints !== void 0 && step.constraints.length > 0 ? { constraints: step.constraints } : {},
4181
4285
  ...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
4182
4286
  };
4183
4287
  let planner;