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.
- package/CONTEXT.md +1 -1
- package/ETHOS.md +1 -1
- package/README.md +12 -18
- package/bootstrap.ts +73 -148
- package/docs/HOW-IT-WORKS.md +162 -0
- package/docs/adr/ADR-0001-rebuild-vs-increment.md +30 -0
- package/docs/adr/ADR-0002-routing-graph-vs-linear-chain.md +36 -0
- package/docs/adr/ADR-0003-per-directory-plan-storage.md +34 -0
- package/docs/adr/ADR-0004-composer-fragment-architecture.md +42 -0
- package/docs/adr/ADR-0005-hook-system-design.md +42 -0
- package/docs/adr/README.md +9 -0
- package/harness/codex/AUTOPILOT.md +30 -23
- package/harness/codex/CHARTER.md +3 -3
- package/harness/lib/composer/compose.test.ts +11 -0
- package/harness/lib/composer/fragments/02-delegation.md +2 -1
- package/harness/lib/composer/fragments/04-task-flow.md +42 -2
- package/harness/lib/composer/fragments/08-routing.md +1 -1
- package/harness/lib/composer/fragments/09-guardrails.md +17 -4
- package/harness/lib/composer/index.ts +1 -1
- package/harness/lib/guards/guard-config.ts +72 -0
- package/harness/lib/hooks/builtins/confidence-gate-hook.ts +2 -4
- package/harness/lib/hooks/builtins/delegation-depth-hook.ts +23 -4
- package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -0
- package/harness/lib/hooks/builtins/next-route-hook.ts +24 -0
- package/harness/lib/hooks/builtins/plan-check-hook.ts +2 -2
- package/harness/lib/hooks/builtins/route-tracking-hook.ts +79 -25
- package/harness/lib/hooks/hooks.test.ts +117 -205
- package/harness/lib/hooks/index.ts +38 -30
- package/harness/lib/hooks/registry.ts +309 -416
- package/harness/lib/hooks/types.ts +116 -71
- package/harness/lib/plans/plan-location.ts +134 -0
- package/harness/lib/routing/index.ts +21 -0
- package/harness/lib/routing/route-guidance.ts +147 -0
- package/harness/lib/routing/route-resolver.ts +58 -0
- package/harness/lib/routing/routing.test.ts +195 -0
- package/harness/lib/routing/skill-frontmatter.ts +125 -0
- package/harness/lib/routing/types.ts +52 -0
- package/harness/skills/oh-ascii/SKILL.md +1 -1
- package/harness/skills/oh-fusion/DEEP.md +56 -33
- package/harness/skills/oh-fusion/SKILL.md +30 -16
- package/harness/skills/oh-init/DEEP.md +2 -2
- package/harness/skills/oh-manifest/SKILL.md +1 -0
- package/harness/skills/oh-plan-review/DEEP.md +1 -1
- package/harness/skills/oh-planner/DEEP.md +3 -3
- package/harness/skills/oh-review/DEEP.md +2 -0
- package/harness/skills/oh-review/SKILL.md +1 -0
- package/package.json +56 -55
- package/harness/lib/background/background.test.ts +0 -197
- package/harness/lib/background/index.ts +0 -7
- package/harness/lib/background/interfaces.ts +0 -31
- package/harness/lib/background/manager.ts +0 -320
- package/harness/lib/hooks/builtins/error-recovery-hook.ts +0 -107
- package/harness/lib/hooks/builtins/memory-sync-hook.ts +0 -73
- package/harness/lib/hooks/builtins/sanity-check-hook.ts +0 -52
- package/harness/lib/memory/index.ts +0 -18
- package/harness/lib/memory/interfaces.ts +0 -53
- package/harness/lib/memory/memory-manager.ts +0 -205
- package/harness/lib/memory/memory.test.ts +0 -491
- package/harness/lib/memory/plan-store.ts +0 -366
- package/harness/lib/recovery/handler.ts +0 -243
- package/harness/lib/recovery/index.ts +0 -14
- package/harness/lib/recovery/interfaces.ts +0 -48
- package/harness/lib/recovery/patterns.ts +0 -149
- package/harness/lib/recovery/recovery.test.ts +0 -312
- package/harness/lib/sanity/anomaly-tracker.ts +0 -127
- package/harness/lib/sanity/checker.ts +0 -178
- package/harness/lib/sanity/index.ts +0 -13
- package/harness/lib/sanity/interfaces.ts +0 -24
- package/harness/lib/sanity/sanity.test.ts +0 -472
- package/harness/lib/sync/file-watcher.ts +0 -174
- package/harness/lib/sync/index.ts +0 -11
- package/harness/lib/sync/interfaces.ts +0 -27
- package/harness/lib/sync/plan-sync.ts +0 -536
- package/harness/lib/sync/sync.test.ts +0 -832
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { HookPhase, HookResult } from "../types.ts";
|
|
3
|
+
import type { HookContext, PostToolUseHook } from "../types.ts";
|
|
4
|
+
import { readSkillFrontmatter, resolveRoute } from "../../routing/index.ts";
|
|
5
|
+
import type { RouteEvidence } from "../../routing/index.ts";
|
|
6
|
+
import { ROUTE_GUIDANCE_PREFIX } from "../../routing/index.ts";
|
|
7
|
+
import {
|
|
8
|
+
ROUTE_ACTIONS,
|
|
9
|
+
ROUTE_OUTCOMES,
|
|
10
|
+
ROUTE_VERIFICATIONS,
|
|
11
|
+
ROUTE_WORK_TYPES,
|
|
12
|
+
} from "../../routing/types.ts";
|
|
13
|
+
|
|
14
|
+
const ROUTE_EVIDENCE_PREFIX = "ROUTE_EVIDENCE:";
|
|
15
|
+
|
|
16
|
+
function isRouteOutcome(value: unknown): value is RouteEvidence["outcome"] {
|
|
17
|
+
return typeof value === "string" && ROUTE_OUTCOMES.includes(value as RouteEvidence["outcome"]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isRouteVerification(value: unknown): value is NonNullable<RouteEvidence["verification"]> {
|
|
21
|
+
return typeof value === "string" && ROUTE_VERIFICATIONS.includes(value as NonNullable<RouteEvidence["verification"]>);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRouteAction(value: unknown): value is NonNullable<RouteEvidence["action"]> {
|
|
25
|
+
return typeof value === "string" && ROUTE_ACTIONS.includes(value as NonNullable<RouteEvidence["action"]>);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isRouteWork(value: unknown): value is NonNullable<RouteEvidence["work"]> {
|
|
29
|
+
return typeof value === "string" && ROUTE_WORK_TYPES.includes(value as NonNullable<RouteEvidence["work"]>);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseRouteEvidence(output: string): RouteEvidence | null {
|
|
33
|
+
const evidenceLine = output
|
|
34
|
+
.split(/\r?\n/)
|
|
35
|
+
.map((line) => line.trim())
|
|
36
|
+
.find((line) => line.startsWith(ROUTE_EVIDENCE_PREFIX));
|
|
37
|
+
|
|
38
|
+
if (!evidenceLine) return null;
|
|
39
|
+
|
|
40
|
+
const raw = evidenceLine.slice(ROUTE_EVIDENCE_PREFIX.length).trim();
|
|
41
|
+
if (!raw) return null;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(raw) as Partial<RouteEvidence>;
|
|
45
|
+
if (!isRouteOutcome(parsed.outcome)) return null;
|
|
46
|
+
if (parsed.verification !== undefined && !isRouteVerification(parsed.verification)) return null;
|
|
47
|
+
if (parsed.action !== undefined && !isRouteAction(parsed.action)) return null;
|
|
48
|
+
if (parsed.work !== undefined && !isRouteWork(parsed.work)) return null;
|
|
49
|
+
if (parsed.target !== undefined && typeof parsed.target !== "string") return null;
|
|
50
|
+
if (parsed.reason !== undefined && typeof parsed.reason !== "string") return null;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
outcome: parsed.outcome,
|
|
54
|
+
...(parsed.verification ? { verification: parsed.verification } : {}),
|
|
55
|
+
...(parsed.action ? { action: parsed.action } : {}),
|
|
56
|
+
...(parsed.work ? { work: parsed.work } : {}),
|
|
57
|
+
...(parsed.target ? { target: parsed.target } : {}),
|
|
58
|
+
...(parsed.reason ? { reason: parsed.reason } : {}),
|
|
59
|
+
};
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const dynamicRouteHook: PostToolUseHook = {
|
|
66
|
+
metadata: {
|
|
67
|
+
name: "dynamic-route",
|
|
68
|
+
priority: 20,
|
|
69
|
+
phase: HookPhase.LATE,
|
|
70
|
+
dependencies: [],
|
|
71
|
+
errorHandling: "isolate",
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async execute(context: HookContext, output: string) {
|
|
75
|
+
const evidence = parseRouteEvidence(output);
|
|
76
|
+
const skillsDir = typeof context._routingSkillsDir === "string" ? context._routingSkillsDir : undefined;
|
|
77
|
+
|
|
78
|
+
if (!evidence || !skillsDir || !context.agent) {
|
|
79
|
+
return { result: HookResult.CONTINUE };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const skillFilePath = path.join(skillsDir, context.agent, "SKILL.md");
|
|
83
|
+
const frontmatter = readSkillFrontmatter(skillFilePath);
|
|
84
|
+
if (!frontmatter) {
|
|
85
|
+
return { result: HookResult.CONTINUE };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const resolution = resolveRoute(frontmatter.route, evidence);
|
|
89
|
+
const guidance = `${ROUTE_GUIDANCE_PREFIX} ${JSON.stringify(resolution)}`;
|
|
90
|
+
const modifiedOutput = output.includes(ROUTE_GUIDANCE_PREFIX)
|
|
91
|
+
? output
|
|
92
|
+
: `${output.trimEnd()}\n${guidance}`.trim();
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
result: HookResult.INJECT,
|
|
96
|
+
modifiedOutput,
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HookPhase, HookResult } from "../types.ts";
|
|
2
|
+
import type { HookContext, RouteHook } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
export const nextRouteHook: RouteHook = {
|
|
5
|
+
metadata: {
|
|
6
|
+
name: "next-route",
|
|
7
|
+
priority: 90,
|
|
8
|
+
phase: HookPhase.EARLY,
|
|
9
|
+
dependencies: [],
|
|
10
|
+
errorHandling: "isolate",
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
async execute(context: HookContext, route: string) {
|
|
14
|
+
const nextRoute = context._nextRoute?.selected;
|
|
15
|
+
if (!nextRoute || nextRoute === route) {
|
|
16
|
+
return { result: HookResult.CONTINUE, modifiedRoute: route };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
result: HookResult.CONTINUE,
|
|
21
|
+
modifiedRoute: nextRoute,
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { HookPhase, HookResult } from "../types.ts";
|
|
9
9
|
import type { HookContext, PreToolUseHook } from "../types.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { resolvePlanAccess } from "../../plans/plan-location.ts";
|
|
11
11
|
|
|
12
12
|
export const planCheckHook: PreToolUseHook = {
|
|
13
13
|
metadata: {
|
|
@@ -19,7 +19,7 @@ export const planCheckHook: PreToolUseHook = {
|
|
|
19
19
|
},
|
|
20
20
|
|
|
21
21
|
async execute(context: HookContext) {
|
|
22
|
-
const planFile =
|
|
22
|
+
const planFile = resolvePlanAccess(context.directory)?.path ?? null;
|
|
23
23
|
|
|
24
24
|
if (!planFile) {
|
|
25
25
|
return {
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
// RouteTrackingHook — RouteHook, priority=55, phase=LATE
|
|
3
3
|
//
|
|
4
4
|
// Loop guard — mechanically enforce two limits:
|
|
5
|
-
// 1. Same skill visited
|
|
6
|
-
// 2.
|
|
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.
|
|
7
10
|
// ---------------------------------------------------------------------------
|
|
8
11
|
|
|
9
12
|
import { HookPhase, HookResult } from "../types.ts";
|
|
10
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";
|
|
11
16
|
|
|
12
17
|
// ---------------------------------------------------------------------------
|
|
13
18
|
// Types
|
|
@@ -55,6 +60,53 @@ export function getHopHistory(sessionId: string): HopRecord[] {
|
|
|
55
60
|
|
|
56
61
|
const defaultArtifactCheck: (route: string) => boolean = () => false;
|
|
57
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
|
+
|
|
58
110
|
// ---------------------------------------------------------------------------
|
|
59
111
|
// Hook
|
|
60
112
|
// ---------------------------------------------------------------------------
|
|
@@ -88,13 +140,9 @@ export const routeTrackingHook: RouteHook = {
|
|
|
88
140
|
sessionStates.set(sessionId, state);
|
|
89
141
|
}
|
|
90
142
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
(context._routeTrackingConfig ?? {}) as RouteTrackingConfig;
|
|
95
|
-
const maxSkillRepeats = config.maxSkillRepeats ?? 5;
|
|
96
|
-
const maxUnproductiveHops = config.maxUnproductiveHops ?? 8;
|
|
97
|
-
const artifactCheck = config.artifactCheck ?? defaultArtifactCheck;
|
|
143
|
+
// Resolve config values
|
|
144
|
+
const { maxSkillRepeats, maxUnproductiveHops, artifactCheck } = resolveMaxValues(context);
|
|
145
|
+
const gc: GuardConfig = context._guardConfig ?? DEFAULT_GUARD_CONFIG;
|
|
98
146
|
|
|
99
147
|
// Record the hop
|
|
100
148
|
const producedArtifact = await artifactCheck(route);
|
|
@@ -116,29 +164,35 @@ export const routeTrackingHook: RouteHook = {
|
|
|
116
164
|
state.unproductiveCount += 1;
|
|
117
165
|
}
|
|
118
166
|
|
|
119
|
-
// Check 1: Same skill repeated too many times
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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})`,
|
|
126
177
|
maxSkillRepeats,
|
|
127
178
|
maxUnproductiveHops,
|
|
128
|
-
|
|
179
|
+
);
|
|
129
180
|
return { result: HookResult.STOP };
|
|
130
181
|
}
|
|
131
182
|
|
|
132
|
-
// Check 2: Too many consecutive unproductive hops
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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})`,
|
|
139
193
|
maxSkillRepeats,
|
|
140
194
|
maxUnproductiveHops,
|
|
141
|
-
|
|
195
|
+
);
|
|
142
196
|
return { result: HookResult.STOP };
|
|
143
197
|
}
|
|
144
198
|
|