openhermes 4.11.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 (46) hide show
  1. package/CONTEXT.md +6 -6
  2. package/ETHOS.md +2 -2
  3. package/README.md +8 -8
  4. package/bootstrap.ts +131 -198
  5. package/harness/codex/AUTOPILOT.md +39 -27
  6. package/harness/codex/CHARTER.md +1 -1
  7. package/harness/lib/background/background.test.ts +24 -5
  8. package/harness/lib/background/manager.ts +9 -9
  9. package/harness/lib/composer/compose.test.ts +29 -18
  10. package/harness/lib/composer/fragments/02-delegation.md +5 -4
  11. package/harness/lib/composer/fragments/04-task-flow.md +43 -3
  12. package/harness/lib/composer/fragments/09-guardrails.md +25 -12
  13. package/harness/lib/guards/guard-config.ts +72 -0
  14. package/harness/lib/hooks/builtins/confidence-gate-hook.ts +9 -11
  15. package/harness/lib/hooks/builtins/delegation-depth-hook.ts +24 -5
  16. package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -0
  17. package/harness/lib/hooks/builtins/error-recovery-hook.ts +7 -7
  18. package/harness/lib/hooks/builtins/memory-sync-hook.ts +2 -2
  19. package/harness/lib/hooks/builtins/next-route-hook.ts +24 -0
  20. package/harness/lib/hooks/builtins/plan-check-hook.ts +5 -5
  21. package/harness/lib/hooks/builtins/route-tracking-hook.ts +80 -26
  22. package/harness/lib/hooks/builtins/subagent-failure-hook.ts +93 -0
  23. package/harness/lib/hooks/hooks.test.ts +145 -69
  24. package/harness/lib/hooks/index.ts +12 -0
  25. package/harness/lib/hooks/registry.ts +3 -3
  26. package/harness/lib/hooks/types.ts +50 -2
  27. package/harness/lib/memory/memory-manager.ts +2 -2
  28. package/harness/lib/memory/memory.test.ts +0 -6
  29. package/harness/lib/memory/plan-store.ts +1 -21
  30. package/harness/lib/plans/plan-location.ts +134 -0
  31. package/harness/lib/routing/index.ts +21 -0
  32. package/harness/lib/routing/route-guidance.ts +147 -0
  33. package/harness/lib/routing/route-resolver.ts +58 -0
  34. package/harness/lib/routing/routing.test.ts +195 -0
  35. package/harness/lib/routing/skill-frontmatter.ts +125 -0
  36. package/harness/lib/routing/types.ts +52 -0
  37. package/harness/lib/sanity/checker.ts +45 -34
  38. package/harness/lib/sync/file-watcher.ts +26 -25
  39. package/harness/lib/sync/plan-sync.ts +22 -25
  40. package/harness/lib/sync/sync.test.ts +30 -4
  41. package/harness/skills/oh-fusion/DEEP.md +109 -86
  42. package/harness/skills/oh-fusion/SKILL.md +47 -33
  43. package/harness/skills/oh-manifest/SKILL.md +1 -0
  44. package/harness/skills/oh-review/DEEP.md +5 -3
  45. package/harness/skills/oh-review/SKILL.md +1 -0
  46. package/package.json +53 -55
@@ -0,0 +1,125 @@
1
+ import fs from "node:fs";
2
+ import type { RouteOutcome, SkillRouteMap, SkillRoutingFrontmatter } from "./types.ts";
3
+
4
+ const EMPTY_ROUTES: SkillRouteMap = {
5
+ pass: [],
6
+ fail: [],
7
+ blocker: [],
8
+ };
9
+
10
+ function stripQuotes(value: string): string {
11
+ return value.trim().replace(/^['"]|['"]$/g, "");
12
+ }
13
+
14
+ function parseRouteValue(value: string): string[] {
15
+ const trimmed = value.trim();
16
+ if (!trimmed) return [];
17
+
18
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
19
+ return trimmed
20
+ .slice(1, -1)
21
+ .split(",")
22
+ .map((entry) => stripQuotes(entry))
23
+ .filter(Boolean);
24
+ }
25
+
26
+ return [stripQuotes(trimmed)].filter(Boolean);
27
+ }
28
+
29
+ function isRouteOutcome(value: string): value is RouteOutcome {
30
+ return value === "pass" || value === "fail" || value === "blocker";
31
+ }
32
+
33
+ export function extractFrontmatter(source: string): string | null {
34
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
35
+ return match?.[1] ?? null;
36
+ }
37
+
38
+ export function parseSkillFrontmatter(source: string): SkillRoutingFrontmatter | null {
39
+ const rawFrontmatter = extractFrontmatter(source);
40
+ if (!rawFrontmatter) return null;
41
+
42
+ const route: SkillRouteMap = {
43
+ pass: [],
44
+ fail: [],
45
+ blocker: [],
46
+ };
47
+
48
+ let name: string | undefined;
49
+ let description: string | undefined;
50
+ let tier: string | undefined;
51
+ let inRouteBlock = false;
52
+ let activeRouteKey: RouteOutcome | null = null;
53
+
54
+ for (const rawLine of rawFrontmatter.split(/\r?\n/)) {
55
+ const line = rawLine.trimEnd();
56
+ const trimmed = line.trim();
57
+
58
+ if (!trimmed) continue;
59
+
60
+ if (/^route:\s*$/.test(trimmed)) {
61
+ inRouteBlock = true;
62
+ activeRouteKey = null;
63
+ continue;
64
+ }
65
+
66
+ if (inRouteBlock) {
67
+ const routeMatch = line.match(/^\s{2,}(pass|fail|blocker):\s*(.*)$/);
68
+ if (routeMatch && isRouteOutcome(routeMatch[1])) {
69
+ activeRouteKey = routeMatch[1];
70
+ route[activeRouteKey].push(...parseRouteValue(routeMatch[2]));
71
+ continue;
72
+ }
73
+
74
+ const listMatch = line.match(/^\s{4,}-\s+(.+)$/);
75
+ if (listMatch && activeRouteKey) {
76
+ route[activeRouteKey].push(stripQuotes(listMatch[1]));
77
+ continue;
78
+ }
79
+
80
+ if (!/^\s/.test(line)) {
81
+ inRouteBlock = false;
82
+ activeRouteKey = null;
83
+ } else {
84
+ activeRouteKey = null;
85
+ continue;
86
+ }
87
+ }
88
+
89
+ const fieldMatch = line.match(/^(name|description|tier):\s*(.+)$/);
90
+ if (!fieldMatch) continue;
91
+
92
+ const value = stripQuotes(fieldMatch[2]);
93
+ switch (fieldMatch[1]) {
94
+ case "name":
95
+ name = value;
96
+ break;
97
+ case "description":
98
+ description = value;
99
+ break;
100
+ case "tier":
101
+ tier = value;
102
+ break;
103
+ }
104
+ }
105
+
106
+ return {
107
+ name,
108
+ description,
109
+ tier,
110
+ route,
111
+ };
112
+ }
113
+
114
+ export function readSkillFrontmatter(skillFilePath: string): SkillRoutingFrontmatter | null {
115
+ if (!fs.existsSync(skillFilePath)) return null;
116
+ return parseSkillFrontmatter(fs.readFileSync(skillFilePath, "utf8"));
117
+ }
118
+
119
+ export function emptySkillRoutes(): SkillRouteMap {
120
+ return {
121
+ pass: [...EMPTY_ROUTES.pass],
122
+ fail: [...EMPTY_ROUTES.fail],
123
+ blocker: [...EMPTY_ROUTES.blocker],
124
+ };
125
+ }
@@ -0,0 +1,52 @@
1
+ export const ROUTE_OUTCOMES = ["pass", "fail", "blocker"] as const;
2
+ export const ROUTE_VERIFICATIONS = ["verified", "unverified"] as const;
3
+ export const ROUTE_ACTIONS = ["done", "fixable", "needs-context", "blocked"] as const;
4
+ export const ROUTE_WORK_TYPES = ["implement", "verify", "ship", "diagnose", "surface"] as const;
5
+
6
+ export type RouteOutcome = (typeof ROUTE_OUTCOMES)[number];
7
+ export type RouteVerification = (typeof ROUTE_VERIFICATIONS)[number];
8
+ export type RouteAction = (typeof ROUTE_ACTIONS)[number];
9
+ export type RouteWork = (typeof ROUTE_WORK_TYPES)[number];
10
+
11
+ export interface SkillRouteMap {
12
+ pass: string[];
13
+ fail: string[];
14
+ blocker: string[];
15
+ }
16
+
17
+ export interface SkillRoutingFrontmatter {
18
+ name?: string;
19
+ description?: string;
20
+ tier?: string;
21
+ route: SkillRouteMap;
22
+ }
23
+
24
+ export interface RouteEvidence {
25
+ outcome: RouteOutcome;
26
+ verification?: RouteVerification;
27
+ action?: RouteAction;
28
+ work?: RouteWork;
29
+ target?: string;
30
+ reason?: string;
31
+ }
32
+
33
+ export interface RouteResolution {
34
+ outcome: RouteOutcome;
35
+ verification?: RouteVerification;
36
+ action?: RouteAction;
37
+ work?: RouteWork;
38
+ candidates: string[];
39
+ selected: string | null;
40
+ reason: string;
41
+ }
42
+
43
+ export interface RuntimeRouteDecision {
44
+ selected: string;
45
+ source: "next_route" | "route_guidance";
46
+ outcome?: RouteOutcome;
47
+ verification?: RouteVerification;
48
+ action?: RouteAction;
49
+ work?: RouteWork;
50
+ candidates?: string[];
51
+ reason?: string;
52
+ }
@@ -16,10 +16,10 @@ import { AnomalyTracker } from "./anomaly-tracker.ts";
16
16
  *
17
17
  * Accepts an optional AnomalyTracker for cross-invocation dedup detection.
18
18
  */
19
- export function checkOutputSanity(
20
- text: unknown,
21
- anomalyTracker?: AnomalyTracker,
22
- ): SanityResult {
19
+ export function checkOutputSanity(
20
+ text: unknown,
21
+ anomalyTracker?: AnomalyTracker,
22
+ ): SanityResult {
23
23
  if (typeof text !== "string") {
24
24
  return {
25
25
  isHealthy: false,
@@ -113,36 +113,47 @@ export function checkOutputSanity(
113
113
  }
114
114
  }
115
115
 
116
- // ═══════════════════════════════════════════════════════════════════
117
- // WARNING checks — mild or context-dependent issues
118
- // ═══════════════════════════════════════════════════════════════════
119
-
120
- // ── 6. Excessive JSON/error stack lines ─────────────────────────
121
- const errorStackLines = text.split(/\r?\n/).filter(
122
- (l) => l.includes("Error:") || l.trim().startsWith("at ") || l.includes("Exception:"),
123
- );
124
- if (errorStackLines.length > 5) {
125
- return {
126
- isHealthy: false,
127
- severity: "warning",
128
- reason: `Error stack bleed detected: ${errorStackLines.length} error/stack lines`,
129
- patternName: "error_stack_bleed",
130
- };
131
- }
132
-
133
- // ── 7. Line-by-line repetition ──────────────────────────────────
134
- const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 10);
135
- if (lines.length > 10) {
136
- const uniqueLines = new Set(lines);
137
- if (uniqueLines.size < lines.length * 0.2) {
138
- return {
139
- isHealthy: false,
140
- severity: "warning",
141
- reason: `Excessive line repetition: ${uniqueLines.size} unique lines out of ${lines.length} (${(uniqueLines.size / lines.length * 100).toFixed(0)}% unique)`,
142
- patternName: "line_repetition",
143
- };
144
- }
145
- }
116
+ // ═══════════════════════════════════════════════════════════════════
117
+ // WARNING checks — mild or context-dependent issues
118
+ // ═══════════════════════════════════════════════════════════════════
119
+
120
+ // ── 6. Excessive JSON/error stack lines ─────────────────────────
121
+ const lines = text.split(/\r?\n/);
122
+ let errorStackLineCount = 0;
123
+ const repetitionLines: string[] = [];
124
+
125
+ for (const line of lines) {
126
+ const trimmed = line.trim();
127
+ if (line.includes("Error:") || trimmed.startsWith("at ") || line.includes("Exception:")) {
128
+ errorStackLineCount++;
129
+ }
130
+
131
+ if (trimmed.length > 10) {
132
+ repetitionLines.push(line);
133
+ }
134
+ }
135
+
136
+ if (errorStackLineCount > 5) {
137
+ return {
138
+ isHealthy: false,
139
+ severity: "warning",
140
+ reason: `Error stack bleed detected: ${errorStackLineCount} error/stack lines`,
141
+ patternName: "error_stack_bleed",
142
+ };
143
+ }
144
+
145
+ // ── 7. Line-by-line repetition ──────────────────────────────────
146
+ if (repetitionLines.length > 10) {
147
+ const uniqueLines = new Set(repetitionLines);
148
+ if (uniqueLines.size < repetitionLines.length * 0.2) {
149
+ return {
150
+ isHealthy: false,
151
+ severity: "warning",
152
+ reason: `Excessive line repetition: ${uniqueLines.size} unique lines out of ${repetitionLines.length} (${(uniqueLines.size / repetitionLines.length * 100).toFixed(0)}% unique)`,
153
+ patternName: "line_repetition",
154
+ };
155
+ }
156
+ }
146
157
 
147
158
  // ── 8. Empty/tiny output ────────────────────────────────────────
148
159
  // Only flag if the entire output is small enough to be suspicious
@@ -14,8 +14,8 @@ const DEBOUNCE_MS = 500; // Debounce window for file-change events
14
14
  // PlanFileWatcher
15
15
  // ---------------------------------------------------------------------------
16
16
 
17
- export class PlanFileWatcher {
18
- private static instance: PlanFileWatcher;
17
+ export class PlanFileWatcher {
18
+ private static instance: PlanFileWatcher | null = null;
19
19
 
20
20
  /** Active fs.FSWatcher instances keyed by directory path. */
21
21
  private watchers = new Map<string, fs.FSWatcher>();
@@ -39,14 +39,14 @@ export class PlanFileWatcher {
39
39
  return PlanFileWatcher.instance;
40
40
  }
41
41
 
42
- /** Reset singleton — used in tests. */
43
- static resetInstance(): void {
44
- const inst = PlanFileWatcher.instance;
45
- if (inst) {
46
- inst.destroy();
47
- PlanFileWatcher.instance = null as unknown as PlanFileWatcher;
48
- }
49
- }
42
+ /** Reset singleton — used in tests. */
43
+ static resetInstance(): void {
44
+ const inst = PlanFileWatcher.instance;
45
+ if (inst) {
46
+ inst.destroy();
47
+ PlanFileWatcher.instance = null;
48
+ }
49
+ }
50
50
 
51
51
  // -----------------------------------------------------------------------
52
52
  // Public API
@@ -120,21 +120,22 @@ export class PlanFileWatcher {
120
120
  this.callbacks.delete(directory);
121
121
  }
122
122
 
123
- /**
124
- * Pause change-notification without losing watch-registrations.
125
- * While paused, events are still debounced but callbacks are suppressed.
126
- */
127
- pause(): void {
128
- this._paused = true;
129
- }
130
-
131
- /**
132
- * Resume change-notification after a pause.
133
- * Pending debounced callbacks will fire on the next event.
134
- */
135
- resume(): void {
136
- this._paused = false;
137
- }
123
+ /**
124
+ * Pause change-notification without losing watch-registrations.
125
+ * While paused, events are still debounced but callbacks are suppressed.
126
+ * Missed changes are not queued or replayed on resume.
127
+ */
128
+ pause(): void {
129
+ this._paused = true;
130
+ }
131
+
132
+ /**
133
+ * Resume change-notification after a pause.
134
+ * Only future events will notify callbacks.
135
+ */
136
+ resume(): void {
137
+ this._paused = false;
138
+ }
138
139
 
139
140
  /** Check if the watcher is currently paused. */
140
141
  get paused(): boolean {
@@ -34,7 +34,7 @@ function normalizeEol(text: string): string {
34
34
  // ---------------------------------------------------------------------------
35
35
 
36
36
  export class PlanSync {
37
- private static instance: PlanSync;
37
+ private static instance: PlanSync | null = null;
38
38
 
39
39
  private constructor() {}
40
40
 
@@ -48,7 +48,7 @@ export class PlanSync {
48
48
 
49
49
  /** Reset singleton — used in tests to get a clean slate. */
50
50
  static resetInstance(): void {
51
- PlanSync.instance = null as unknown as PlanSync;
51
+ PlanSync.instance = null;
52
52
  }
53
53
 
54
54
  // -----------------------------------------------------------------------
@@ -286,24 +286,19 @@ export class PlanSync {
286
286
  // Parse per-entry metadata (version, description, status)
287
287
  const entryMeta = new Map<string, Record<string, string>>();
288
288
  for (const line of metaLines) {
289
- const ev = line.match(/^entry-(task-\d+)-version:\s*(\d+)$/i);
290
- if (ev) {
291
- const m = entryMeta.get(ev[1]) ?? {};
292
- m.version = ev[2];
293
- entryMeta.set(ev[1], m);
294
- }
295
- const ed = line.match(/^entry-(task-\d+)-description:\s*(.+)$/i);
296
- if (ed) {
297
- const m = entryMeta.get(ed[1]) ?? {};
298
- m.description = ed[2].trim();
299
- entryMeta.set(ed[1], m);
300
- }
301
- const es = line.match(/^entry-(task-\d+)-status:\s*(.+)$/i);
302
- if (es) {
303
- const m = entryMeta.get(es[1]) ?? {};
304
- m.status = es[2].trim().toLowerCase();
305
- entryMeta.set(es[1], m);
289
+ const keyMatch = line.match(/^entry-(task-\d+)-(version|description|status):\s*(.+)$/i);
290
+ if (!keyMatch) continue;
291
+
292
+ const m = entryMeta.get(keyMatch[1]) ?? {};
293
+ if (keyMatch[2] === "status") {
294
+ m.status = keyMatch[3].trim().toLowerCase();
295
+ } else if (keyMatch[2] === "description") {
296
+ m.description = keyMatch[3].trim();
297
+ } else {
298
+ m.version = keyMatch[3].trim();
306
299
  }
300
+
301
+ entryMeta.set(keyMatch[1], m);
307
302
  }
308
303
 
309
304
  // ---- 2. Find relevant sections ----
@@ -374,6 +369,8 @@ export class PlanSync {
374
369
  * each starting with `### Task N:`.
375
370
  */
376
371
  private splitTaskBlocks(tasksContent: string): string[] {
372
+ if (!tasksContent.includes("### Task ")) return [];
373
+
377
374
  // Split on lines starting with `### `
378
375
  const parts = tasksContent.split(/\n(?=### )/);
379
376
  return parts.filter((p) => /^###\s+Task\s+\d+\s*:/m.test(p));
@@ -392,16 +389,16 @@ export class PlanSync {
392
389
  completedContent: string | null,
393
390
  activeContent: string | null,
394
391
  ): SyncPlanEntry["status"] {
392
+ const taskRef = new RegExp(`Task\\s+${taskNum}\\s*:`, "i");
393
+
395
394
  // Check Completed section
396
- if (completedContent) {
397
- const re = new RegExp(`Task\\s+${taskNum}\\s*:`, "i");
398
- if (re.test(completedContent)) return "completed";
395
+ if (completedContent && taskRef.test(completedContent)) {
396
+ return "completed";
399
397
  }
400
398
 
401
399
  // Check Active Task section
402
- if (activeContent) {
403
- const re = new RegExp(`Task\\s+${taskNum}\\s*:`, "i");
404
- if (re.test(activeContent)) return "in_progress";
400
+ if (activeContent && taskRef.test(activeContent)) {
401
+ return "in_progress";
405
402
  }
406
403
 
407
404
  // Check success-criteria checkboxes
@@ -776,6 +776,11 @@ describe("PlanFileWatcher", () => {
776
776
  await delay(800);
777
777
 
778
778
  const countDuringPause = callbacks.length;
779
+ assert.equal(
780
+ countDuringPause,
781
+ 0,
782
+ "pause should suppress callbacks while active",
783
+ );
779
784
 
780
785
  // Resume
781
786
  watcher.resume();
@@ -785,10 +790,31 @@ describe("PlanFileWatcher", () => {
785
790
  await fs.promises.writeFile(filePath, content + "\n\n\n\n", "utf8");
786
791
  await delay(800);
787
792
 
788
- // The pause should have prevented the first write's callback
789
- // (but note: fs.watch on Windows may batch events; we verify pause
790
- // at least prevented the callback that would have fired during pause)
791
- assert.ok(watcher.paused === false, "watcher should not be paused after resume");
793
+ assert.ok(
794
+ callbacks.length > countDuringPause,
795
+ "resume should allow new callbacks; paused changes are not replayed",
796
+ );
797
+ assert.equal(watcher.paused, false);
798
+ } finally {
799
+ await cleanup();
800
+ }
801
+ });
802
+
803
+ it("resetInstance returns a fresh watcher with cleared state", async () => {
804
+ const content = makePlanContent([{ num: 1, title: "Reset" }]);
805
+ const { dir, cleanup } = await createTestPlan(content);
806
+
807
+ try {
808
+ const watcher = PlanFileWatcher.getInstance();
809
+ watcher.watch(dir, () => {});
810
+ watcher.pause();
811
+
812
+ PlanFileWatcher.resetInstance();
813
+
814
+ const fresh = PlanFileWatcher.getInstance();
815
+ assert.notEqual(fresh, watcher);
816
+ assert.equal(fresh.paused, false);
817
+ assert.equal(fresh.watchedDirectories().length, 0);
792
818
  } finally {
793
819
  await cleanup();
794
820
  }