openhermes 4.11.2 → 4.13.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 (74) hide show
  1. package/CONTEXT.md +1 -1
  2. package/ETHOS.md +1 -1
  3. package/README.md +12 -18
  4. package/bootstrap.ts +73 -148
  5. package/docs/HOW-IT-WORKS.md +162 -0
  6. package/docs/adr/ADR-0001-rebuild-vs-increment.md +30 -0
  7. package/docs/adr/ADR-0002-routing-graph-vs-linear-chain.md +36 -0
  8. package/docs/adr/ADR-0003-per-directory-plan-storage.md +34 -0
  9. package/docs/adr/ADR-0004-composer-fragment-architecture.md +42 -0
  10. package/docs/adr/ADR-0005-hook-system-design.md +42 -0
  11. package/docs/adr/README.md +9 -0
  12. package/harness/codex/AUTOPILOT.md +30 -23
  13. package/harness/codex/CHARTER.md +3 -3
  14. package/harness/lib/composer/compose.test.ts +11 -0
  15. package/harness/lib/composer/fragments/02-delegation.md +2 -1
  16. package/harness/lib/composer/fragments/04-task-flow.md +42 -2
  17. package/harness/lib/composer/fragments/08-routing.md +1 -1
  18. package/harness/lib/composer/fragments/09-guardrails.md +17 -4
  19. package/harness/lib/composer/index.ts +1 -1
  20. package/harness/lib/guards/guard-config.ts +72 -0
  21. package/harness/lib/hooks/builtins/confidence-gate-hook.ts +2 -4
  22. package/harness/lib/hooks/builtins/delegation-depth-hook.ts +23 -4
  23. package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -0
  24. package/harness/lib/hooks/builtins/next-route-hook.ts +24 -0
  25. package/harness/lib/hooks/builtins/plan-check-hook.ts +2 -2
  26. package/harness/lib/hooks/builtins/route-tracking-hook.ts +79 -25
  27. package/harness/lib/hooks/hooks.test.ts +117 -205
  28. package/harness/lib/hooks/index.ts +38 -30
  29. package/harness/lib/hooks/registry.ts +309 -416
  30. package/harness/lib/hooks/types.ts +116 -71
  31. package/harness/lib/plans/plan-location.ts +134 -0
  32. package/harness/lib/routing/index.ts +21 -0
  33. package/harness/lib/routing/route-guidance.ts +147 -0
  34. package/harness/lib/routing/route-resolver.ts +58 -0
  35. package/harness/lib/routing/routing.test.ts +195 -0
  36. package/harness/lib/routing/skill-frontmatter.ts +125 -0
  37. package/harness/lib/routing/types.ts +52 -0
  38. package/harness/skills/oh-ascii/SKILL.md +1 -1
  39. package/harness/skills/oh-fusion/DEEP.md +56 -33
  40. package/harness/skills/oh-fusion/SKILL.md +30 -16
  41. package/harness/skills/oh-init/DEEP.md +2 -2
  42. package/harness/skills/oh-manifest/SKILL.md +1 -0
  43. package/harness/skills/oh-plan-review/DEEP.md +1 -1
  44. package/harness/skills/oh-planner/DEEP.md +3 -3
  45. package/harness/skills/oh-review/DEEP.md +2 -0
  46. package/harness/skills/oh-review/SKILL.md +1 -0
  47. package/package.json +56 -55
  48. package/harness/lib/background/background.test.ts +0 -197
  49. package/harness/lib/background/index.ts +0 -7
  50. package/harness/lib/background/interfaces.ts +0 -31
  51. package/harness/lib/background/manager.ts +0 -320
  52. package/harness/lib/hooks/builtins/error-recovery-hook.ts +0 -107
  53. package/harness/lib/hooks/builtins/memory-sync-hook.ts +0 -73
  54. package/harness/lib/hooks/builtins/sanity-check-hook.ts +0 -52
  55. package/harness/lib/memory/index.ts +0 -18
  56. package/harness/lib/memory/interfaces.ts +0 -53
  57. package/harness/lib/memory/memory-manager.ts +0 -205
  58. package/harness/lib/memory/memory.test.ts +0 -491
  59. package/harness/lib/memory/plan-store.ts +0 -366
  60. package/harness/lib/recovery/handler.ts +0 -243
  61. package/harness/lib/recovery/index.ts +0 -14
  62. package/harness/lib/recovery/interfaces.ts +0 -48
  63. package/harness/lib/recovery/patterns.ts +0 -149
  64. package/harness/lib/recovery/recovery.test.ts +0 -312
  65. package/harness/lib/sanity/anomaly-tracker.ts +0 -127
  66. package/harness/lib/sanity/checker.ts +0 -178
  67. package/harness/lib/sanity/index.ts +0 -13
  68. package/harness/lib/sanity/interfaces.ts +0 -24
  69. package/harness/lib/sanity/sanity.test.ts +0 -472
  70. package/harness/lib/sync/file-watcher.ts +0 -174
  71. package/harness/lib/sync/index.ts +0 -11
  72. package/harness/lib/sync/interfaces.ts +0 -27
  73. package/harness/lib/sync/plan-sync.ts +0 -536
  74. package/harness/lib/sync/sync.test.ts +0 -832
@@ -1,107 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // ErrorRecoveryHook — PostToolUse, priority=50, phase=LATE
3
- //
4
- // After sub-agent call, check if output indicates error.
5
- // Use the RecoveryHandler to match patterns and inject recovery actions.
6
- // ---------------------------------------------------------------------------
7
-
8
- import { HookPhase, HookResult } from "../types.ts";
9
- import type { HookContext, PostToolUseHook } from "../types.ts";
10
- import { RecoveryHandler } from "../../recovery/handler.ts";
11
- import type { ErrorContext } from "../../recovery/interfaces.ts";
12
-
13
- export const errorRecoveryHook: PostToolUseHook = {
14
- metadata: {
15
- name: "error-recovery",
16
- priority: 50,
17
- phase: HookPhase.LATE,
18
- dependencies: [],
19
- errorHandling: "isolate",
20
- },
21
-
22
- async execute(context: HookContext, output: string) {
23
- // Check if the output looks like an error
24
- const isErrorOutput = looksLikeError(output);
25
- if (!isErrorOutput) {
26
- return { result: HookResult.CONTINUE };
27
- }
28
-
29
- // Classify the error and get a recovery action
30
- const handler = RecoveryHandler.getInstance();
31
- const errorContext: ErrorContext = {
32
- sessionId: context.sessionId,
33
- error: new Error(output.slice(0, 500)), // Truncate for classification
34
- attempt: (context._recoveryAttempt as number) ?? 0,
35
- timestamp: Date.now(),
36
- agent: context.agent,
37
- };
38
-
39
- const action = handler.handleError(errorContext);
40
-
41
- // Build a recovery instruction based on the action
42
- const recoveryInstruction = buildRecoveryInstruction(action);
43
-
44
- return {
45
- result: HookResult.INJECT,
46
- modifiedOutput: output,
47
- injectRecovery: recoveryInstruction,
48
- };
49
- },
50
- };
51
-
52
- /**
53
- * Heuristic check: does the output look like an error?
54
- * Looks for common error patterns in tool output.
55
- */
56
- function looksLikeError(output: string): boolean {
57
- if (!output || output.length === 0) return false;
58
-
59
- const errorPatterns = [
60
- /error/i,
61
- /exception/i,
62
- /failed/i,
63
- /failure/i,
64
- /unable to/i,
65
- /could not/i,
66
- /not found/i,
67
- /ECONNREFUSED/i,
68
- /ETIMEDOUT/i,
69
- /rate.?limited/i,
70
- /too many requests/i,
71
- /context.?length/i,
72
- /token.?limit/i,
73
- /parse.?error/i,
74
- /syntax.?error/i,
75
- /timeout/i,
76
- /execution.?timed.?out/i,
77
- ];
78
-
79
- // Check first 2000 chars to avoid false positives in long output
80
- const head = output.slice(0, 2000);
81
- return errorPatterns.some((p) => p.test(head));
82
- }
83
-
84
- /**
85
- * Build a recovery instruction string from a RecoveryAction.
86
- */
87
- function buildRecoveryInstruction(
88
- action: { type: string; delay?: number; maxAttempts?: number; reason: string; modifyPrompt?: string },
89
- ): string {
90
- const parts: string[] = [
91
- `[HOOK: Error Recovery]`,
92
- `Action: ${action.type}`,
93
- `Reason: ${action.reason}`,
94
- ];
95
-
96
- if (action.delay) {
97
- parts.push(`Delay: ${action.delay}ms before retry`);
98
- }
99
- if (action.maxAttempts) {
100
- parts.push(`Max attempts: ${action.maxAttempts}`);
101
- }
102
- if (action.modifyPrompt) {
103
- parts.push(`Modification: ${action.modifyPrompt}`);
104
- }
105
-
106
- return parts.join("\n");
107
- }
@@ -1,73 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // MemorySyncHook — PostToolUse, priority=40, phase=LATE
3
- //
4
- // After each step, sync memory entries to plan file.
5
- // Uses MemoryManager from harness/lib/memory/memory-manager.ts
6
- // ---------------------------------------------------------------------------
7
-
8
- import { HookPhase, HookResult } from "../types.ts";
9
- import type { HookContext, PostToolUseHook } from "../types.ts";
10
- import { MemoryManager } from "../../memory/memory-manager.ts";
11
- import { PlanStore } from "../../memory/plan-store.ts";
12
- import { findLatestPlanFile } from "../../../../bootstrap.ts";
13
- import { MemoryLevel } from "../../memory/interfaces.ts";
14
-
15
- export const memorySyncHook: PostToolUseHook = {
16
- metadata: {
17
- name: "memory-sync",
18
- priority: 40,
19
- phase: HookPhase.LATE,
20
- dependencies: [],
21
- errorHandling: "isolate",
22
- },
23
-
24
- async execute(context: HookContext, output: string) {
25
- // Sync memory entries to plan file
26
- const planFile = findLatestPlanFile(context.directory);
27
- if (!planFile) {
28
- // No plan file to sync to — skip silently
29
- return { result: HookResult.CONTINUE };
30
- }
31
-
32
- const mem = MemoryManager.getInstance();
33
-
34
- // Extract findings from the session memory (TASK level)
35
- const taskEntries = mem.getEntries(MemoryLevel.TASK);
36
-
37
- // Sync important task entries as plan findings
38
- for (const entry of taskEntries) {
39
- if (entry.importance >= 0.6) {
40
- try {
41
- await PlanStore.addFinding(planFile, context.sessionId, {
42
- description: entry.content,
43
- severity: entry.importance >= 0.8 ? "warning" : "info",
44
- });
45
- } catch {
46
- // Plan sync is best-effort — don't break execution
47
- }
48
- }
49
- }
50
-
51
- // Sync any decisions
52
- const missionEntries = mem.getEntries(MemoryLevel.MISSION);
53
- for (const entry of missionEntries) {
54
- if (entry.metadata?.type === "decision" && entry.importance >= 0.7) {
55
- try {
56
- await PlanStore.addDecision(planFile, context.sessionId, {
57
- description: entry.content,
58
- rationale: (entry.metadata.rationale as string) ?? "Auto-synced decision",
59
- });
60
- } catch {
61
- // Best-effort
62
- }
63
- }
64
- }
65
-
66
- return {
67
- result: HookResult.CONTINUE,
68
- modifiedContext: {
69
- _memorySyncCount: taskEntries.filter((e) => e.importance >= 0.6).length,
70
- },
71
- };
72
- },
73
- };
@@ -1,52 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // SanityCheckHook — PostToolUse, priority=30, phase=LATE
3
- //
4
- // After each tool invocation, check the output for LLM degeneration patterns
5
- // (repetition, low diversity, gibberish, etc.). Track consecutive anomalies
6
- // per session. If 2+ consecutive anomalies detected, inject a recovery
7
- // instruction to compact/refresh context before it cascades.
8
- // ---------------------------------------------------------------------------
9
-
10
- import { HookPhase, HookResult } from "../types.ts";
11
- import type { HookContext, PostToolUseHook } from "../types.ts";
12
- import { checkOutputSanity } from "../../sanity/checker.ts";
13
- import { AnomalyTracker } from "../../sanity/anomaly-tracker.ts";
14
-
15
- export const sanityCheckHook: PostToolUseHook = {
16
- metadata: {
17
- name: "sanity-check",
18
- priority: 30,
19
- phase: HookPhase.LATE,
20
- dependencies: [],
21
- errorHandling: "isolate",
22
- },
23
-
24
- async execute(context: HookContext, output: string) {
25
- const sessionId = context.sessionId;
26
-
27
- // Run the sanity checker on the output
28
- const result = checkOutputSanity(output);
29
-
30
- // Record the result in the anomaly tracker
31
- const tracker = AnomalyTracker.getInstance();
32
- const tracking = tracker.record(sessionId, result);
33
-
34
- if (result.isHealthy) {
35
- // Output is healthy — no action needed
36
- return { result: HookResult.CONTINUE };
37
- }
38
-
39
- // Unhealthy output detected
40
- if (tracking.shouldEscalate) {
41
- // Escalation threshold reached — inject recovery instruction
42
- return {
43
- result: HookResult.INJECT,
44
- modifiedOutput: output,
45
- injectRecovery: tracking.recoveryMessage ?? "recovery: compact context",
46
- };
47
- }
48
-
49
- // First anomaly (below threshold) — let it pass with a warning
50
- return { result: HookResult.CONTINUE };
51
- },
52
- };
@@ -1,18 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Memory module — barrel export
3
- // ---------------------------------------------------------------------------
4
-
5
- export {
6
- MemoryLevel,
7
- DEFAULT_BUDGETS,
8
- } from "./interfaces.ts";
9
- export type {
10
- MemoryEntry,
11
- MemorySnapshot,
12
- MemoryConfig,
13
- Finding,
14
- Decision,
15
- } from "./interfaces.ts";
16
-
17
- export { MemoryManager } from "./memory-manager.ts";
18
- export { PlanStore } from "./plan-store.ts";
@@ -1,53 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // 4-Tier Memory System — interfaces & types
3
- // ---------------------------------------------------------------------------
4
-
5
- export enum MemoryLevel {
6
- SYSTEM = "system", // immutable OpenHermes identity, never pruned
7
- PROJECT = "project", // project-level config, conventions, decisions
8
- MISSION = "mission", // current session goal, active plan reference
9
- TASK = "task", // per-step findings, cleared after each iteration
10
- }
11
-
12
- export interface MemoryEntry {
13
- id: string;
14
- level: MemoryLevel;
15
- content: string;
16
- timestamp: number;
17
- importance: number; // 0.0 to 1.0
18
- metadata?: Record<string, string>;
19
- }
20
-
21
- export interface MemorySnapshot {
22
- system: MemoryEntry[];
23
- project: MemoryEntry[];
24
- mission: MemoryEntry[];
25
- task: MemoryEntry[];
26
- }
27
-
28
- export interface MemoryConfig {
29
- budgets: Partial<Record<MemoryLevel, number>>; // max entries per level, defaults filled for missing
30
- }
31
-
32
- export interface Finding {
33
- id: string;
34
- sessionId: string;
35
- description: string;
36
- severity: "info" | "warning" | "blocker";
37
- timestamp: number;
38
- }
39
-
40
- export interface Decision {
41
- id: string;
42
- sessionId: string;
43
- description: string;
44
- rationale: string;
45
- timestamp: number;
46
- }
47
-
48
- export const DEFAULT_BUDGETS: Record<MemoryLevel, number> = {
49
- [MemoryLevel.SYSTEM]: 50,
50
- [MemoryLevel.PROJECT]: 100,
51
- [MemoryLevel.MISSION]: 30,
52
- [MemoryLevel.TASK]: 20,
53
- };
@@ -1,205 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // MemoryManager — singleton 4-tier hierarchical memory store
3
- // ---------------------------------------------------------------------------
4
-
5
- import { randomUUID } from "node:crypto";
6
- import {
7
- MemoryLevel,
8
- DEFAULT_BUDGETS,
9
- } from "./interfaces.ts";
10
- import type {
11
- MemoryEntry,
12
- MemorySnapshot,
13
- MemoryConfig,
14
- } from "./interfaces.ts";
15
-
16
- // ---------------------------------------------------------------------------
17
- // Manager
18
- // ---------------------------------------------------------------------------
19
-
20
- export class MemoryManager {
21
- private static instance: MemoryManager;
22
-
23
- private entries: Map<MemoryLevel, MemoryEntry[]> = new Map();
24
- private config: MemoryConfig;
25
-
26
- private constructor(config?: Partial<MemoryConfig>) {
27
- this.config = {
28
- budgets: { ...DEFAULT_BUDGETS, ...config?.budgets },
29
- };
30
- // Initialise every level so callers never hit undefined
31
- for (const level of Object.values(MemoryLevel)) {
32
- this.entries.set(level, []);
33
- }
34
- }
35
-
36
- // -----------------------------------------------------------------------
37
- // Singleton
38
- // -----------------------------------------------------------------------
39
-
40
- static getInstance(config?: Partial<MemoryConfig>): MemoryManager {
41
- if (!MemoryManager.instance) {
42
- MemoryManager.instance = new MemoryManager(config);
43
- }
44
- return MemoryManager.instance;
45
- }
46
-
47
- /** Reset singleton — used in tests for isolation. */
48
- static resetInstance(): void {
49
- MemoryManager.instance = null as unknown as MemoryManager;
50
- }
51
-
52
- // -----------------------------------------------------------------------
53
- // Public API
54
- // -----------------------------------------------------------------------
55
-
56
- /**
57
- * Add a memory entry at the given level.
58
- * Inserts, sorts by importance DESC then timestamp DESC, then prunes.
59
- */
60
- add(
61
- level: MemoryLevel,
62
- content: string,
63
- importance: number,
64
- metadata?: Record<string, string>,
65
- ): MemoryEntry {
66
- const entry: MemoryEntry = {
67
- id: randomUUID(),
68
- level,
69
- content,
70
- timestamp: Date.now(),
71
- importance: Math.max(0, Math.min(1, importance)), // clamp [0, 1]
72
- metadata,
73
- };
74
-
75
- const bucket = this.entries.get(level)!;
76
- bucket.push(entry);
77
-
78
- // Sort: importance DESC, then timestamp DESC (newer first for ties)
79
- bucket.sort((a, b) => {
80
- if (b.importance !== a.importance) return b.importance - a.importance;
81
- return b.timestamp - a.timestamp;
82
- });
83
-
84
- this.prune(level);
85
- return entry;
86
- }
87
-
88
- /**
89
- * Return a formatted context string for all levels.
90
- * If a query string is provided, only entries whose content includes
91
- * the query (case-insensitive) are returned.
92
- */
93
- getContext(query?: string): string {
94
- const parts: string[] = [];
95
-
96
- for (const level of Object.values(MemoryLevel)) {
97
- const bucket = this.entries.get(level) ?? [];
98
- let filtered = bucket;
99
-
100
- if (query && query.length > 0) {
101
- const q = query.toLowerCase();
102
- filtered = bucket.filter((e) => e.content.toLowerCase().includes(q));
103
- }
104
-
105
- if (filtered.length === 0) continue;
106
-
107
- const heading = level.toUpperCase();
108
- const lines = filtered.map((e) => {
109
- const imp = e.importance.toFixed(2);
110
- const meta = e.metadata ? ` [${formatMetadata(e.metadata)}]` : "";
111
- return ` [${imp}] ${e.content}${meta}`;
112
- });
113
-
114
- parts.push(`== ${heading} ==\n${lines.join("\n")}`);
115
- }
116
-
117
- return parts.join("\n\n");
118
- }
119
-
120
- /**
121
- * Remove the least important entries from a level when budget is exceeded.
122
- * Falls back to DEFAULT_BUDGETS if no explicit budget is configured.
123
- */
124
- prune(level: MemoryLevel): void {
125
- const budget = this.config.budgets[level] ?? DEFAULT_BUDGETS[level];
126
-
127
- // Guard: budget must be a valid non-negative number
128
- if (typeof budget !== "number" || budget < 0 || !Number.isFinite(budget)) {
129
- console.warn(
130
- `[MemoryManager] Invalid budget for level "${level}": ${budget}. Skipping prune.`,
131
- );
132
- return;
133
- }
134
-
135
- const bucket = this.entries.get(level);
136
- if (!bucket) return;
137
- if (bucket.length <= budget) return;
138
-
139
- // Already sorted: importance DESC → drop from the end
140
- bucket.splice(budget);
141
- }
142
-
143
- /** Clear all entries at a given level (used for TASK after iteration). */
144
- clearLevel(level: MemoryLevel): void {
145
- this.entries.set(level, []);
146
- }
147
-
148
- /** Serialise all entries into a snapshot. */
149
- export(): MemorySnapshot {
150
- return {
151
- system: [...(this.entries.get(MemoryLevel.SYSTEM) ?? [])],
152
- project: [...(this.entries.get(MemoryLevel.PROJECT) ?? [])],
153
- mission: [...(this.entries.get(MemoryLevel.MISSION) ?? [])],
154
- task: [...(this.entries.get(MemoryLevel.TASK) ?? [])],
155
- };
156
- }
157
-
158
- /** Restore state from a snapshot. */
159
- import(snapshot: MemorySnapshot): void {
160
- this.entries.set(MemoryLevel.SYSTEM, [...snapshot.system]);
161
- this.entries.set(MemoryLevel.PROJECT, [...snapshot.project]);
162
- this.entries.set(MemoryLevel.MISSION, [...snapshot.mission]);
163
- this.entries.set(MemoryLevel.TASK, [...snapshot.task]);
164
- }
165
-
166
- /** Get entries, optionally filtered by level. */
167
- getEntries(level?: MemoryLevel): MemoryEntry[] {
168
- if (level) {
169
- return [...(this.entries.get(level) ?? [])];
170
- }
171
- const all: MemoryEntry[] = [];
172
- for (const lvl of Object.values(MemoryLevel)) {
173
- all.push(...(this.entries.get(lvl) ?? []));
174
- }
175
- return all;
176
- }
177
-
178
- /** Count entries at a given level. */
179
- getEntryCount(level: MemoryLevel): number {
180
- return this.entries.get(level)?.length ?? 0;
181
- }
182
-
183
- /** Update the budgets after construction. */
184
- setBudgets(budgets: Partial<Record<MemoryLevel, number>>): void {
185
- for (const [level, budget] of Object.entries(budgets)) {
186
- if (budget !== undefined && Object.values(MemoryLevel).includes(level as MemoryLevel)) {
187
- this.config.budgets[level as MemoryLevel] = budget;
188
- }
189
- }
190
- // Re-prune all levels with new budgets
191
- for (const level of Object.values(MemoryLevel)) {
192
- this.prune(level);
193
- }
194
- }
195
- }
196
-
197
- // ---------------------------------------------------------------------------
198
- // Helpers
199
- // ---------------------------------------------------------------------------
200
-
201
- function formatMetadata(meta: Record<string, string>): string {
202
- return Object.entries(meta)
203
- .map(([k, v]) => `${k}=${v}`)
204
- .join(", ");
205
- }