openhermes 4.12.1 → 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 (73) hide show
  1. package/CONTEXT.md +6 -6
  2. package/ETHOS.md +2 -2
  3. package/README.md +11 -17
  4. package/bootstrap.ts +118 -126
  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 +35 -40
  13. package/harness/codex/CHARTER.md +3 -3
  14. package/harness/lib/composer/compose.test.ts +29 -29
  15. package/harness/lib/composer/fragments/02-delegation.md +5 -5
  16. package/harness/lib/composer/fragments/04-task-flow.md +13 -13
  17. package/harness/lib/composer/fragments/08-routing.md +1 -1
  18. package/harness/lib/composer/fragments/09-guardrails.md +25 -25
  19. package/harness/lib/composer/index.ts +1 -1
  20. package/harness/lib/guards/guard-config.ts +72 -72
  21. package/harness/lib/hooks/builtins/confidence-gate-hook.ts +9 -9
  22. package/harness/lib/hooks/builtins/delegation-depth-hook.ts +1 -1
  23. package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -99
  24. package/harness/lib/hooks/builtins/next-route-hook.ts +24 -24
  25. package/harness/lib/hooks/builtins/plan-check-hook.ts +5 -5
  26. package/harness/lib/hooks/builtins/route-tracking-hook.ts +1 -1
  27. package/harness/lib/hooks/hooks.test.ts +160 -324
  28. package/harness/lib/hooks/index.ts +38 -42
  29. package/harness/lib/hooks/registry.ts +309 -416
  30. package/harness/lib/hooks/types.ts +116 -119
  31. package/harness/lib/plans/plan-location.ts +134 -134
  32. package/harness/lib/routing/index.ts +21 -21
  33. package/harness/lib/routing/route-guidance.ts +147 -147
  34. package/harness/lib/routing/route-resolver.ts +58 -58
  35. package/harness/lib/routing/routing.test.ts +195 -195
  36. package/harness/lib/routing/skill-frontmatter.ts +125 -125
  37. package/harness/lib/routing/types.ts +52 -52
  38. package/harness/skills/oh-ascii/SKILL.md +1 -1
  39. package/harness/skills/oh-fusion/DEEP.md +109 -109
  40. package/harness/skills/oh-fusion/SKILL.md +47 -47
  41. package/harness/skills/oh-init/DEEP.md +2 -2
  42. package/harness/skills/oh-plan-review/DEEP.md +1 -1
  43. package/harness/skills/oh-planner/DEEP.md +3 -3
  44. package/harness/skills/oh-review/DEEP.md +5 -5
  45. package/package.json +56 -53
  46. package/harness/lib/background/background.test.ts +0 -216
  47. package/harness/lib/background/index.ts +0 -7
  48. package/harness/lib/background/interfaces.ts +0 -31
  49. package/harness/lib/background/manager.ts +0 -320
  50. package/harness/lib/hooks/builtins/error-recovery-hook.ts +0 -107
  51. package/harness/lib/hooks/builtins/memory-sync-hook.ts +0 -73
  52. package/harness/lib/hooks/builtins/sanity-check-hook.ts +0 -52
  53. package/harness/lib/hooks/builtins/subagent-failure-hook.ts +0 -93
  54. package/harness/lib/memory/index.ts +0 -18
  55. package/harness/lib/memory/interfaces.ts +0 -53
  56. package/harness/lib/memory/memory-manager.ts +0 -205
  57. package/harness/lib/memory/memory.test.ts +0 -485
  58. package/harness/lib/memory/plan-store.ts +0 -346
  59. package/harness/lib/recovery/handler.ts +0 -243
  60. package/harness/lib/recovery/index.ts +0 -14
  61. package/harness/lib/recovery/interfaces.ts +0 -48
  62. package/harness/lib/recovery/patterns.ts +0 -149
  63. package/harness/lib/recovery/recovery.test.ts +0 -312
  64. package/harness/lib/sanity/anomaly-tracker.ts +0 -127
  65. package/harness/lib/sanity/checker.ts +0 -189
  66. package/harness/lib/sanity/index.ts +0 -13
  67. package/harness/lib/sanity/interfaces.ts +0 -24
  68. package/harness/lib/sanity/sanity.test.ts +0 -472
  69. package/harness/lib/sync/file-watcher.ts +0 -175
  70. package/harness/lib/sync/index.ts +0 -11
  71. package/harness/lib/sync/interfaces.ts +0 -27
  72. package/harness/lib/sync/plan-sync.ts +0 -533
  73. package/harness/lib/sync/sync.test.ts +0 -858
@@ -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 { resolvePlanAccess } from "../../plans/plan-location.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 = resolvePlanAccess(context.directory)?.path ?? null;
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,93 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // SubagentFailureHook — PostToolUse, priority=45, phase=LATE
3
- //
4
- // Mechanically tracks subagent task failures.
5
- // At maxSubagentFailures consecutive failures on the same task,
6
- // surfaces a BLOCKER.
7
- // ---------------------------------------------------------------------------
8
-
9
- import { HookPhase, HookResult } from "../types.ts";
10
- import type { HookContext, PostToolUseHook } from "../types.ts";
11
- import { DEFAULT_GUARD_CONFIG, checkGuardProgression } from "../../guards/guard-config.ts";
12
- import type { GuardConfig } from "../../guards/guard-config.ts";
13
-
14
- /** Module-level failure tracker — maps sessionId to consecutive failure count */
15
- const failureCounters = new Map<string, number>();
16
-
17
- export function resetSubagentFailures(sessionId?: string): void {
18
- if (sessionId) {
19
- failureCounters.delete(sessionId);
20
- } else {
21
- failureCounters.clear();
22
- }
23
- }
24
-
25
- export function getSubagentFailureCount(sessionId: string): number {
26
- return failureCounters.get(sessionId) ?? 0;
27
- }
28
-
29
- // Error pattern detection — reuses the same patterns from error-recovery-hook
30
- function outputLooksLikeFailure(output: string): boolean {
31
- if (!output || output.length === 0) return true; // Empty output = failure
32
- const errorPatterns = [
33
- /error/i, /exception/i, /failed/i, /failure/i,
34
- /unable to/i, /could not/i, /not found/i,
35
- /ECONNREFUSED/i, /ETIMEDOUT/i, /rate.?limited/i,
36
- /too many requests/i, /timeout/i, /execution.?timed.?out/i,
37
- ];
38
- const head = output.slice(0, 2000);
39
- return errorPatterns.some((p) => p.test(head));
40
- }
41
-
42
- export const subagentFailureHook: PostToolUseHook = {
43
- metadata: {
44
- name: "subagent-failure",
45
- priority: 45,
46
- phase: HookPhase.LATE,
47
- dependencies: [],
48
- errorHandling: "isolate",
49
- },
50
-
51
- async execute(context: HookContext, output: string) {
52
- const sessionId = context.sessionId;
53
- const config: GuardConfig = context._guardConfig ?? DEFAULT_GUARD_CONFIG;
54
- const maxFailures = config.maxSubagentFailures;
55
-
56
- if (maxFailures <= 0) {
57
- // Disabled
58
- return { result: HookResult.CONTINUE };
59
- }
60
-
61
- const isFailure = outputLooksLikeFailure(output);
62
- let currentCount = failureCounters.get(sessionId) ?? 0;
63
-
64
- if (isFailure) {
65
- currentCount += 1;
66
- failureCounters.set(sessionId, currentCount);
67
- } else {
68
- // Success — reset counter
69
- failureCounters.set(sessionId, 0);
70
- return { result: HookResult.CONTINUE };
71
- }
72
-
73
- // Check progression
74
- const progression = checkGuardProgression(currentCount, maxFailures, config);
75
-
76
- if (progression.level === "stop") {
77
- // Surface BLOCKER
78
- return {
79
- result: HookResult.INJECT,
80
- modifiedOutput: output,
81
- injectRecovery: `[HOOK: BLOCKER] ${currentCount} consecutive subagent failures (max ${maxFailures}). Surface to orchestrator with findings and stop delegating.`,
82
- };
83
- }
84
-
85
- if (progression.level === "warn" || progression.level === "escalate") {
86
- // Annotate but don't stop
87
- context._guardProgression = progression;
88
- context._subagentFailures = currentCount;
89
- }
90
-
91
- return { result: HookResult.CONTINUE };
92
- },
93
- };
@@ -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 | null = null;
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;
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
- }