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.
- package/CONTEXT.md +6 -6
- package/ETHOS.md +2 -2
- package/README.md +8 -8
- package/bootstrap.ts +131 -198
- package/harness/codex/AUTOPILOT.md +39 -27
- package/harness/codex/CHARTER.md +1 -1
- package/harness/lib/background/background.test.ts +24 -5
- package/harness/lib/background/manager.ts +9 -9
- package/harness/lib/composer/compose.test.ts +29 -18
- package/harness/lib/composer/fragments/02-delegation.md +5 -4
- package/harness/lib/composer/fragments/04-task-flow.md +43 -3
- package/harness/lib/composer/fragments/09-guardrails.md +25 -12
- package/harness/lib/guards/guard-config.ts +72 -0
- package/harness/lib/hooks/builtins/confidence-gate-hook.ts +9 -11
- package/harness/lib/hooks/builtins/delegation-depth-hook.ts +24 -5
- package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -0
- package/harness/lib/hooks/builtins/error-recovery-hook.ts +7 -7
- package/harness/lib/hooks/builtins/memory-sync-hook.ts +2 -2
- package/harness/lib/hooks/builtins/next-route-hook.ts +24 -0
- package/harness/lib/hooks/builtins/plan-check-hook.ts +5 -5
- package/harness/lib/hooks/builtins/route-tracking-hook.ts +80 -26
- package/harness/lib/hooks/builtins/subagent-failure-hook.ts +93 -0
- package/harness/lib/hooks/hooks.test.ts +145 -69
- package/harness/lib/hooks/index.ts +12 -0
- package/harness/lib/hooks/registry.ts +3 -3
- package/harness/lib/hooks/types.ts +50 -2
- package/harness/lib/memory/memory-manager.ts +2 -2
- package/harness/lib/memory/memory.test.ts +0 -6
- package/harness/lib/memory/plan-store.ts +1 -21
- 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/lib/sanity/checker.ts +45 -34
- package/harness/lib/sync/file-watcher.ts +26 -25
- package/harness/lib/sync/plan-sync.ts +22 -25
- package/harness/lib/sync/sync.test.ts +30 -4
- package/harness/skills/oh-fusion/DEEP.md +109 -86
- package/harness/skills/oh-fusion/SKILL.md +47 -33
- package/harness/skills/oh-manifest/SKILL.md +1 -0
- package/harness/skills/oh-review/DEEP.md +5 -3
- package/harness/skills/oh-review/SKILL.md +1 -0
- 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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
*
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
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
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
m.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
}
|