ennodia 0.1.1 → 0.2.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.
Files changed (46) hide show
  1. package/CONTRIBUTING.md +60 -36
  2. package/README.md +74 -58
  3. package/bench/README.md +7 -5
  4. package/bench/bug-recall/run.ts +12 -4
  5. package/bin/ennodia +23 -2
  6. package/docs/assets/agents/mcp.svg +1 -1
  7. package/docs/assets/orchestration-pipeline.svg +4 -3
  8. package/docs/comparisons/agent-frameworks.md +15 -10
  9. package/docs/comparisons/autogen.md +20 -15
  10. package/docs/comparisons/chathub.md +15 -14
  11. package/docs/comparisons/index.md +30 -22
  12. package/docs/comparisons/langgraph.md +19 -12
  13. package/docs/comparisons/mixture-of-agents.md +20 -17
  14. package/docs/comparisons/model-merging.md +7 -5
  15. package/docs/comparisons/openrouter.md +30 -22
  16. package/docs/concepts/compositional-audits.md +16 -13
  17. package/docs/concepts/data-governance.md +31 -15
  18. package/docs/concepts/how-ennodia-works.md +48 -22
  19. package/docs/concepts/interfaces-and-core.md +19 -6
  20. package/docs/concepts/second-opinions.md +20 -20
  21. package/docs/getting-started.md +55 -27
  22. package/docs/guides/agent-skills.md +16 -13
  23. package/docs/guides/budgets-and-limits.md +30 -20
  24. package/docs/guides/running-better-audits.md +50 -44
  25. package/docs/index.md +60 -37
  26. package/docs/install.md +41 -32
  27. package/docs/reference/benchmarks.md +6 -4
  28. package/docs/reference/controlled-english.md +130 -0
  29. package/docs/reference/ennodia-io.md +28 -22
  30. package/docs/reference/mcp-tools.md +244 -87
  31. package/docs/reference/supported-harnesses.md +9 -9
  32. package/package.json +7 -5
  33. package/src/advisor.ts +685 -0
  34. package/src/budget.ts +25 -6
  35. package/src/cli.ts +1 -1
  36. package/src/compare.ts +248 -52
  37. package/src/compositional.ts +18 -0
  38. package/src/core.ts +547 -16
  39. package/src/harnesses.ts +33 -2
  40. package/src/index.ts +38 -0
  41. package/src/plan-advice.ts +601 -0
  42. package/src/runs.ts +74 -9
  43. package/src/server.ts +312 -46
  44. package/src/skills.ts +131 -11
  45. package/src/tasks.ts +192 -22
  46. package/src/version.ts +1 -1
package/src/runs.ts CHANGED
@@ -71,12 +71,17 @@ export type RunStartInput = {
71
71
  mode?: RunMode;
72
72
  compare?: RunCompareMode;
73
73
  cwd?: string;
74
+ isolateCwd?: boolean;
74
75
  model?: string;
75
76
  timeoutMs?: number;
76
77
  refresh?: boolean;
77
78
  judgeHarnessId?: string;
78
79
  judgeModel?: string;
80
+ advisorHarnessId?: string;
81
+ advisorModel?: string;
82
+ /** @deprecated Use advisorHarnessId. */
79
83
  synthesizerHarnessId?: string;
84
+ /** @deprecated Use advisorModel. */
80
85
  synthesizerModel?: string;
81
86
  maxOutputChars?: number;
82
87
  skills?: Skill[];
@@ -94,6 +99,11 @@ export type RunView = {
94
99
  endedAt?: string;
95
100
  elapsedMs: number;
96
101
  plan: RoutePlan;
102
+ /** True when harnessId was explicitly forced, meaning plan.selected /
103
+ * plan.candidates reflect what the auto-router would have chosen and were
104
+ * NOT what actually ran. Check selectedHarnessIds for the harness(es) that
105
+ * actually executed. */
106
+ harnessOverridden: boolean;
97
107
  selectedHarnessIds: string[];
98
108
  taskIds: string[];
99
109
  compareId?: string;
@@ -108,6 +118,11 @@ export type RunView = {
108
118
  eventCount: number;
109
119
  events: RunEvent[];
110
120
  appliedSkills?: AppliedSkillInfo[];
121
+ /** Skills discoverable in cwd's native skill directories but not part of
122
+ * this run's appliedSkills. A harness may self-select these on its own
123
+ * initiative even though this run didn't request them. Populated by
124
+ * EnnodiaCore.startRun; empty when cwd was not provided. */
125
+ unrequestedSkillsPresent?: string[];
111
126
  budget: BudgetCheck;
112
127
  };
113
128
 
@@ -152,6 +167,7 @@ type InternalRun = {
152
167
  compareMode: RunCompareMode;
153
168
  prompt: string;
154
169
  plan: RoutePlan;
170
+ harnessOverridden: boolean;
155
171
  selectedHarnessIds: string[];
156
172
  taskIds: string[];
157
173
  compareId?: string;
@@ -204,6 +220,8 @@ export class RunManager {
204
220
  throw new Error("RunManager is shutting down.");
205
221
  }
206
222
 
223
+ validateRunAdvisorAliases(input);
224
+
207
225
  const harnesses = await this.dependencies.discoverHarnesses({
208
226
  refresh: input.refresh,
209
227
  });
@@ -248,6 +266,7 @@ export class RunManager {
248
266
  compareMode,
249
267
  prompt: input.prompt,
250
268
  plan,
269
+ harnessOverridden: Boolean(input.harnessId),
251
270
  selectedHarnessIds,
252
271
  taskIds: [],
253
272
  createdAtMs: now,
@@ -368,6 +387,7 @@ export class RunManager {
368
387
  const started = this.dependencies.taskManager.start(adapter, discovery, {
369
388
  prompt: input.prompt,
370
389
  cwd: input.cwd,
390
+ isolateCwd: input.isolateCwd,
371
391
  model: input.model,
372
392
  timeoutMs: input.timeoutMs,
373
393
  skills: input.skills,
@@ -466,6 +486,8 @@ export class RunManager {
466
486
  taskIds,
467
487
  judgeHarnessId: input.judgeHarnessId,
468
488
  judgeModel: input.judgeModel,
489
+ advisorHarnessId: input.advisorHarnessId,
490
+ advisorModel: input.advisorModel,
469
491
  synthesizerHarnessId: input.synthesizerHarnessId,
470
492
  synthesizerModel: input.synthesizerModel,
471
493
  cwd: input.cwd,
@@ -485,13 +507,15 @@ export class RunManager {
485
507
  return;
486
508
  }
487
509
 
488
- if (result.status === "succeeded" && result.synthesis?.text.trim()) {
510
+ const finalAnswer = result.advisor?.answer.trim() ||
511
+ result.synthesis?.text.trim();
512
+ if (result.status === "succeeded" && finalAnswer) {
489
513
  this.pushEvent(run, {
490
514
  type: "compare-succeeded",
491
515
  compareId: compare.id,
492
- message: "Compare returned the final answer.",
516
+ message: "Compare returned the Advisor recommendation.",
493
517
  });
494
- this.succeedRun(run, result.synthesis.text.trim());
518
+ this.succeedRun(run, finalAnswer);
495
519
  return;
496
520
  }
497
521
 
@@ -566,6 +590,7 @@ export class RunManager {
566
590
  endedAt: run.endedAtMs ? new Date(run.endedAtMs).toISOString() : undefined,
567
591
  elapsedMs: Math.max(0, end - run.createdAtMs),
568
592
  plan: run.plan,
593
+ harnessOverridden: run.harnessOverridden,
569
594
  selectedHarnessIds: run.selectedHarnessIds,
570
595
  taskIds: run.taskIds,
571
596
  compareId: run.compareId,
@@ -865,21 +890,25 @@ function eventFromTask(task: TaskView): Omit<RunEvent, "at"> {
865
890
  };
866
891
  }
867
892
 
893
+ function taskOutputText(task: TaskView): string {
894
+ return task.finalMessage?.trim() ||
895
+ `${task.stdout.trim()}\n${task.stderr.trim()}`.trim();
896
+ }
897
+
868
898
  function hasComparableOutput(task: TaskView): boolean {
869
- return `${task.stdout.trim()}\n${task.stderr.trim()}`.trim().length > 0;
899
+ return taskOutputText(task).length > 0;
870
900
  }
871
901
 
872
902
  function formatTaskOutputs(tasks: TaskView[]): string {
873
903
  if (tasks.length === 1) {
874
- return `${tasks[0].stdout.trim()}\n${tasks[0].stderr.trim()}`.trim();
904
+ return taskOutputText(tasks[0]);
875
905
  }
876
906
 
877
907
  return tasks
878
908
  .map((task) =>
879
909
  [
880
910
  `## ${task.harnessName} (${task.id})`,
881
- `${task.stdout.trim()}\n${task.stderr.trim()}`.trim() ||
882
- "(no output)",
911
+ taskOutputText(task) || "(no output)",
883
912
  ].join("\n\n")
884
913
  )
885
914
  .join("\n\n");
@@ -911,11 +940,47 @@ function compareTaskIds(compare: CompareView | undefined): string[] {
911
940
  return [];
912
941
  }
913
942
 
914
- return [compare.judgeTaskId, compare.synthesizerTaskId].filter(
915
- (taskId): taskId is string => Boolean(taskId),
943
+ return [
944
+ ...new Set([
945
+ compare.judgeTaskId,
946
+ compare.advisorTaskId,
947
+ compare.synthesizerTaskId,
948
+ ].filter((taskId): taskId is string => Boolean(taskId))),
949
+ ];
950
+ }
951
+
952
+ function validateRunAdvisorAliases(input: RunStartInput): void {
953
+ validateRunAliasPair(
954
+ "advisorHarnessId",
955
+ input.advisorHarnessId,
956
+ "synthesizerHarnessId",
957
+ input.synthesizerHarnessId,
958
+ );
959
+ validateRunAliasPair(
960
+ "advisorModel",
961
+ input.advisorModel,
962
+ "synthesizerModel",
963
+ input.synthesizerModel,
916
964
  );
917
965
  }
918
966
 
967
+ function validateRunAliasPair(
968
+ preferredName: string,
969
+ preferredValue: string | undefined,
970
+ legacyName: string,
971
+ legacyValue: string | undefined,
972
+ ): void {
973
+ if (
974
+ preferredValue !== undefined &&
975
+ legacyValue !== undefined &&
976
+ preferredValue !== legacyValue
977
+ ) {
978
+ throw new Error(
979
+ `Conflicting Run fields: ${preferredName} and deprecated ${legacyName}.`,
980
+ );
981
+ }
982
+ }
983
+
919
984
  function isTerminalRun(run: InternalRun): boolean {
920
985
  return (
921
986
  run.status === "succeeded" ||