openhermes 4.9.2 → 4.12.1

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 (85) hide show
  1. package/CONTEXT.md +7 -7
  2. package/ETHOS.md +2 -2
  3. package/README.md +34 -33
  4. package/bootstrap.ts +310 -160
  5. package/harness/agents/oh-planner.md +1 -1
  6. package/harness/agents/openhermes.md +27 -126
  7. package/harness/codex/AUTOPILOT.md +131 -23
  8. package/harness/codex/CHARTER.md +4 -5
  9. package/harness/lib/background/background.test.ts +216 -0
  10. package/harness/lib/background/index.ts +7 -0
  11. package/harness/lib/background/interfaces.ts +31 -0
  12. package/harness/lib/background/manager.ts +320 -0
  13. package/harness/lib/composer/compose.test.ts +179 -0
  14. package/harness/lib/composer/compose.ts +65 -0
  15. package/harness/lib/composer/fragments/01-identity.md +1 -0
  16. package/harness/lib/composer/fragments/02-delegation.md +7 -0
  17. package/harness/lib/composer/fragments/03-permissions.md +13 -0
  18. package/harness/lib/composer/fragments/04-task-flow.md +55 -0
  19. package/harness/lib/composer/fragments/05-confidence.md +5 -0
  20. package/harness/lib/composer/fragments/06-parallelization.md +17 -0
  21. package/harness/lib/composer/fragments/07-shell.md +41 -0
  22. package/harness/lib/composer/fragments/08-routing.md +8 -0
  23. package/harness/lib/composer/fragments/09-guardrails.md +25 -0
  24. package/harness/lib/composer/index.ts +1 -0
  25. package/harness/lib/guards/guard-config.ts +72 -0
  26. package/harness/lib/hooks/builtins/confidence-gate-hook.ts +68 -0
  27. package/harness/lib/hooks/builtins/delegation-depth-hook.ts +78 -0
  28. package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -0
  29. package/harness/lib/hooks/builtins/error-recovery-hook.ts +107 -0
  30. package/harness/lib/hooks/builtins/memory-sync-hook.ts +73 -0
  31. package/harness/lib/hooks/builtins/next-route-hook.ts +24 -0
  32. package/harness/lib/hooks/builtins/plan-check-hook.ts +43 -0
  33. package/harness/lib/hooks/builtins/route-tracking-hook.ts +201 -0
  34. package/harness/lib/hooks/builtins/sanity-check-hook.ts +52 -0
  35. package/harness/lib/hooks/builtins/shell-detect-hook.ts +96 -0
  36. package/harness/lib/hooks/builtins/subagent-failure-hook.ts +93 -0
  37. package/harness/lib/hooks/hooks.test.ts +1092 -0
  38. package/harness/lib/hooks/index.ts +42 -0
  39. package/harness/lib/hooks/registry.ts +416 -0
  40. package/harness/lib/hooks/types.ts +119 -0
  41. package/harness/lib/memory/index.ts +18 -0
  42. package/harness/lib/memory/interfaces.ts +53 -0
  43. package/harness/lib/memory/memory-manager.ts +205 -0
  44. package/harness/lib/memory/memory.test.ts +485 -0
  45. package/harness/lib/memory/plan-store.ts +346 -0
  46. package/harness/lib/plans/plan-location.ts +134 -0
  47. package/harness/lib/recovery/handler.ts +243 -0
  48. package/harness/lib/recovery/index.ts +14 -0
  49. package/harness/lib/recovery/interfaces.ts +48 -0
  50. package/harness/lib/recovery/patterns.ts +149 -0
  51. package/harness/lib/recovery/recovery.test.ts +312 -0
  52. package/harness/lib/routing/index.ts +21 -0
  53. package/harness/lib/routing/route-guidance.ts +147 -0
  54. package/harness/lib/routing/route-resolver.ts +58 -0
  55. package/harness/lib/routing/routing.test.ts +195 -0
  56. package/harness/lib/routing/skill-frontmatter.ts +125 -0
  57. package/harness/lib/routing/types.ts +52 -0
  58. package/harness/lib/sanity/anomaly-tracker.ts +127 -0
  59. package/harness/lib/sanity/checker.ts +189 -0
  60. package/harness/lib/sanity/index.ts +13 -0
  61. package/harness/lib/sanity/interfaces.ts +24 -0
  62. package/harness/lib/sanity/sanity.test.ts +472 -0
  63. package/harness/lib/sync/file-watcher.ts +175 -0
  64. package/harness/lib/sync/index.ts +11 -0
  65. package/harness/lib/sync/interfaces.ts +27 -0
  66. package/harness/lib/sync/plan-sync.ts +533 -0
  67. package/harness/lib/sync/sync.test.ts +858 -0
  68. package/harness/skills/oh-fusion/DEEP.md +109 -86
  69. package/harness/skills/oh-fusion/SKILL.md +47 -33
  70. package/harness/skills/oh-init/DEEP.md +2 -2
  71. package/harness/skills/oh-manifest/SKILL.md +2 -1
  72. package/harness/skills/oh-plan-review/DEEP.md +1 -1
  73. package/harness/skills/oh-planner/DEEP.md +3 -3
  74. package/harness/skills/oh-review/DEEP.md +5 -3
  75. package/harness/skills/oh-review/SKILL.md +1 -0
  76. package/harness/skills/oh-ship/SKILL.md +1 -1
  77. package/harness/skills/oh-skill-craft/SKILL.md +1 -4
  78. package/package.json +53 -55
  79. package/tsconfig.json +1 -1
  80. package/harness/commands/oh-doctor.md +0 -205
  81. package/harness/commands/oh-log.md +0 -18
  82. package/harness/skills/oh-learn/DEEP.md +0 -44
  83. package/harness/skills/oh-learn/SKILL.md +0 -30
  84. package/scripts/count-tokens.mjs +0 -158
  85. package/scripts/oh-doctor.ps1 +0 -342
@@ -0,0 +1,201 @@
1
+ // ---------------------------------------------------------------------------
2
+ // RouteTrackingHook — RouteHook, priority=55, phase=LATE
3
+ //
4
+ // Loop guard — mechanically enforce two limits:
5
+ // 1. Same skill visited N+ times in one chain (default 5)
6
+ // 2. N+ consecutive unproductive hops (default 8)
7
+ //
8
+ // Config from _guardConfig (centralized) with fallback to _routeTrackingConfig
9
+ // for backward compatibility. Progressive warning at thresholds before hard stop.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ import { HookPhase, HookResult } from "../types.ts";
13
+ import type { HookContext, RouteHook } from "../types.ts";
14
+ import type { GuardConfig, GuardProgression } from "../../guards/guard-config.ts";
15
+ import { checkGuardProgression, DEFAULT_GUARD_CONFIG } from "../../guards/guard-config.ts";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface HopRecord {
22
+ skill: string;
23
+ timestamp: number;
24
+ producedArtifact: boolean;
25
+ }
26
+
27
+ export interface RouteTrackingConfig {
28
+ maxSkillRepeats: number;
29
+ maxUnproductiveHops: number;
30
+ artifactCheck: (route: string) => boolean | Promise<boolean>;
31
+ }
32
+
33
+ interface RouteTrackingState {
34
+ hops: HopRecord[];
35
+ skillCounts: Map<string, number>;
36
+ unproductiveCount: number;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Module-level state
41
+ // ---------------------------------------------------------------------------
42
+
43
+ const sessionStates = new Map<string, RouteTrackingState>();
44
+
45
+ export function resetRouteTracker(sessionId?: string): void {
46
+ if (sessionId) {
47
+ sessionStates.delete(sessionId);
48
+ } else {
49
+ sessionStates.clear();
50
+ }
51
+ }
52
+
53
+ export function getHopHistory(sessionId: string): HopRecord[] {
54
+ return sessionStates.get(sessionId)?.hops ?? [];
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Default artifact check (conservative — assumes unproductive)
59
+ // ---------------------------------------------------------------------------
60
+
61
+ const defaultArtifactCheck: (route: string) => boolean = () => false;
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Resolve max values from guard config with fallbacks
65
+ // ---------------------------------------------------------------------------
66
+
67
+ function resolveMaxValues(context: HookContext): {
68
+ maxSkillRepeats: number;
69
+ maxUnproductiveHops: number;
70
+ artifactCheck: (route: string) => boolean | Promise<boolean>;
71
+ } {
72
+ // Primary: _guardConfig (centralized)
73
+ const gc: GuardConfig = context._guardConfig ?? DEFAULT_GUARD_CONFIG;
74
+ const maxSkillRepeats = gc.maxSkillRepeats;
75
+ const maxUnproductiveHops = gc.maxUnproductiveHops;
76
+
77
+ // Backward compat: _routeTrackingConfig overrides if present
78
+ const legacy = context._routeTrackingConfig as Partial<RouteTrackingConfig> | undefined;
79
+ const artifactCheck = legacy?.artifactCheck ?? defaultArtifactCheck;
80
+ const legacySkillRepeats = legacy?.maxSkillRepeats;
81
+ const legacyUnproductiveHops = legacy?.maxUnproductiveHops;
82
+
83
+ return {
84
+ maxSkillRepeats: legacySkillRepeats ?? maxSkillRepeats,
85
+ maxUnproductiveHops: legacyUnproductiveHops ?? maxUnproductiveHops,
86
+ artifactCheck,
87
+ };
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Build optiRoute report helper
92
+ // ---------------------------------------------------------------------------
93
+
94
+ function buildOptiRouteReport(
95
+ state: RouteTrackingState,
96
+ reason: string,
97
+ maxSkillRepeats: number,
98
+ maxUnproductiveHops: number,
99
+ ) {
100
+ return {
101
+ reason,
102
+ chain: [...state.hops],
103
+ skillCounts: Object.fromEntries(state.skillCounts),
104
+ unproductiveCount: state.unproductiveCount,
105
+ maxSkillRepeats,
106
+ maxUnproductiveHops,
107
+ };
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Hook
112
+ // ---------------------------------------------------------------------------
113
+
114
+ export const routeTrackingHook: RouteHook = {
115
+ metadata: {
116
+ name: "route-tracking",
117
+ priority: 55,
118
+ phase: HookPhase.LATE,
119
+ dependencies: [],
120
+ errorHandling: "propagate",
121
+ },
122
+
123
+ async execute(context: HookContext, route: string) {
124
+ // Skip terminal routes — they don't count as routing hops
125
+ const terminalRoutes = new Set(["surface", "done", "oh-handoff"]);
126
+ if (terminalRoutes.has(route)) {
127
+ return { result: HookResult.CONTINUE, modifiedRoute: route };
128
+ }
129
+
130
+ const sessionId = context.sessionId;
131
+
132
+ // Get or create state for this session
133
+ let state = sessionStates.get(sessionId);
134
+ if (!state) {
135
+ state = {
136
+ hops: [],
137
+ skillCounts: new Map<string, number>(),
138
+ unproductiveCount: 0,
139
+ };
140
+ sessionStates.set(sessionId, state);
141
+ }
142
+
143
+ // Resolve config values
144
+ const { maxSkillRepeats, maxUnproductiveHops, artifactCheck } = resolveMaxValues(context);
145
+ const gc: GuardConfig = context._guardConfig ?? DEFAULT_GUARD_CONFIG;
146
+
147
+ // Record the hop
148
+ const producedArtifact = await artifactCheck(route);
149
+ const hop: HopRecord = {
150
+ skill: route,
151
+ timestamp: Date.now(),
152
+ producedArtifact,
153
+ };
154
+ state.hops.push(hop);
155
+
156
+ // Update skill count
157
+ const currentSkillCount = (state.skillCounts.get(route) ?? 0) + 1;
158
+ state.skillCounts.set(route, currentSkillCount);
159
+
160
+ // Update unproductive counter (resets on any productive hop)
161
+ if (producedArtifact) {
162
+ state.unproductiveCount = 0;
163
+ } else {
164
+ state.unproductiveCount += 1;
165
+ }
166
+
167
+ // Check 1: Same skill repeated too many times — with progressive warning
168
+ let progression = checkGuardProgression(currentSkillCount, maxSkillRepeats, gc);
169
+ if (progression.level === "warn" || progression.level === "escalate") {
170
+ // Progressive warning — annotate context but don't stop
171
+ context._guardProgression = progression;
172
+ }
173
+ if (progression.level === "stop") {
174
+ context._optiRoute = buildOptiRouteReport(
175
+ state,
176
+ `Same skill "${route}" visited ${currentSkillCount} times (max ${maxSkillRepeats})`,
177
+ maxSkillRepeats,
178
+ maxUnproductiveHops,
179
+ );
180
+ return { result: HookResult.STOP };
181
+ }
182
+
183
+ // Check 2: Too many consecutive unproductive hops — with progressive warning
184
+ progression = checkGuardProgression(state.unproductiveCount, maxUnproductiveHops, gc);
185
+ if (progression.level === "warn" || progression.level === "escalate") {
186
+ // Progressive warning — annotate context but don't stop
187
+ context._guardProgression = progression;
188
+ }
189
+ if (progression.level === "stop") {
190
+ context._optiRoute = buildOptiRouteReport(
191
+ state,
192
+ `${state.unproductiveCount} consecutive unproductive hops (max ${maxUnproductiveHops})`,
193
+ maxSkillRepeats,
194
+ maxUnproductiveHops,
195
+ );
196
+ return { result: HookResult.STOP };
197
+ }
198
+
199
+ return { result: HookResult.CONTINUE, modifiedRoute: route };
200
+ },
201
+ };
@@ -0,0 +1,52 @@
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
+ };
@@ -0,0 +1,96 @@
1
+ // ---------------------------------------------------------------------------
2
+ // ShellDetectHook — PreToolUse, priority=80, phase=EARLY
3
+ //
4
+ // Before sub-agent calls that need CLI, inject SHELL.md preamble.
5
+ // Detect platform, add appropriate shell context.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ import { HookPhase, HookResult } from "../types.ts";
9
+ import type { HookContext, PreToolUseHook } from "../types.ts";
10
+ import { getHarnessDir } from "../../../../lib/harness-resolver.ts";
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import os from "node:os";
14
+
15
+ export const shellDetectHook: PreToolUseHook = {
16
+ metadata: {
17
+ name: "shell-detect",
18
+ priority: 80,
19
+ phase: HookPhase.EARLY,
20
+ dependencies: [],
21
+ errorHandling: "isolate",
22
+ },
23
+
24
+ async execute(context: HookContext) {
25
+ const platform = os.platform();
26
+ const isWindows = platform === "win32";
27
+
28
+ // Detect shell type
29
+ let shellType = "unknown";
30
+ if (isWindows) {
31
+ // On Windows: detect PowerShell, CMD, or Git Bash
32
+ const comSpec = process.env.COMSPEC ?? "";
33
+ if (process.env.PSModulePath || process.env.PSExecutionPolicy) {
34
+ shellType = "powershell";
35
+ } else if (comSpec.toLowerCase().includes("cmd")) {
36
+ shellType = "cmd";
37
+ } else {
38
+ // Could be Git Bash
39
+ shellType = process.env.BASH ? "bash" : "powershell";
40
+ }
41
+ } else {
42
+ shellType = "bash";
43
+ }
44
+
45
+ // Try to load SHELL.md
46
+ let shellPreamble = "";
47
+ try {
48
+ const shellDocPath = path.join(getHarnessDir(), "instructions", "SHELL.md");
49
+ await fs.promises.access(shellDocPath);
50
+ shellPreamble = (await fs.promises.readFile(shellDocPath, "utf8")).trim();
51
+ } catch {
52
+ // If SHELL.md can't be read, provide minimal preamble
53
+ shellPreamble = "";
54
+ }
55
+
56
+ return {
57
+ result: HookResult.CONTINUE,
58
+ modifiedContext: {
59
+ _shellPlatform: platform,
60
+ _shellType: shellType,
61
+ _shellPreamble: shellPreamble || getDefaultPreamble(shellType, platform),
62
+ },
63
+ };
64
+ },
65
+ };
66
+
67
+ function getDefaultPreamble(shellType: string, _platform: string): string {
68
+ if (shellType === "powershell") {
69
+ return [
70
+ "## Shell Environment",
71
+ "",
72
+ "Detected: PowerShell on Windows",
73
+ "- File ops, scoop installs, ps1 scripts, env vars → PowerShell",
74
+ "- git, bun, npm, node → any shell (all work)",
75
+ "- rm -rf, make, unix scripts → Git Bash",
76
+ "",
77
+ ].join("\n");
78
+ }
79
+ if (shellType === "cmd") {
80
+ return [
81
+ "## Shell Environment",
82
+ "",
83
+ "Detected: CMD on Windows",
84
+ "- .bat/.cmd scripts → CMD",
85
+ "- git, bun, npm, node → any shell",
86
+ "",
87
+ ].join("\n");
88
+ }
89
+ return [
90
+ "## Shell Environment",
91
+ "",
92
+ `Detected: ${shellType}`,
93
+ "- Standard POSIX shell commands available",
94
+ "",
95
+ ].join("\n");
96
+ }
@@ -0,0 +1,93 @@
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
+ };