principles-disciple 1.61.0 → 1.63.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/openclaw.plugin.json +4 -4
- package/package.json +3 -1
- package/scripts/sync-plugin.mjs +28 -36
- package/src/core/event-log.ts +71 -5
- package/src/core/workflow-funnel-loader.ts +170 -0
- package/src/hooks/gate-block-helper.ts +1 -1
- package/src/hooks/gate.ts +62 -203
- package/src/service/evolution-worker.ts +10 -0
- package/src/service/nocturnal-service.ts +24 -3
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -0
- package/src/types/event-types.ts +103 -2
- package/tests/core/event-log.test.ts +56 -1
- package/tests/hooks/gate-rule-host-pipeline.test.ts +161 -316
- package/tests/service/evolution-worker.compilation-backfill.test.ts +5 -1
- package/src/hooks/bash-risk.ts +0 -175
- package/src/hooks/edit-verification.ts +0 -302
- package/src/hooks/gfi-gate.ts +0 -186
- package/src/hooks/progressive-trust-gate.ts +0 -183
- package/src/hooks/thinking-checkpoint.ts +0 -76
- package/tests/hooks/bash-risk-integration.test.ts +0 -137
- package/tests/hooks/bash-risk.test.ts +0 -81
- package/tests/hooks/edit-verification.test.ts +0 -678
- package/tests/hooks/gate-edit-verification-p1.test.ts +0 -632
- package/tests/hooks/gate-pipeline-integration.test.ts +0 -404
- package/tests/hooks/gate.test.ts +0 -271
- package/tests/hooks/gfi-gate-unit.test.ts +0 -422
- package/tests/hooks/gfi-gate.test.ts +0 -669
- package/tests/hooks/thinking-gate.test.ts +0 -313
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.63.0",
|
|
6
6
|
"skills": [
|
|
7
7
|
"./skills"
|
|
8
8
|
],
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
"buildFingerprint": {
|
|
79
|
-
"gitSha": "
|
|
80
|
-
"bundleMd5": "
|
|
81
|
-
"builtAt": "2026-04-
|
|
79
|
+
"gitSha": "0d6db62f8866",
|
|
80
|
+
"bundleMd5": "9cf803e170837b148578f78ff3c77dbd",
|
|
81
|
+
"builtAt": "2026-04-18T23:04:51.214Z"
|
|
82
82
|
}
|
|
83
83
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "principles-disciple",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.63.0",
|
|
4
4
|
"description": "Native OpenClaw plugin for Principles Disciple",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/bundle.js",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@testing-library/react": "^16.3.0",
|
|
45
45
|
"@types/better-sqlite3": "^7.6.13",
|
|
46
|
+
"@types/js-yaml": "^4.0.9",
|
|
46
47
|
"@types/micromatch": "^4.0.10",
|
|
47
48
|
"@types/node": "^25.6.0",
|
|
48
49
|
"@types/react": "^19.2.2",
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
"@principles/core": "^0.1.0",
|
|
71
72
|
"@sinclair/typebox": "^0.34.48",
|
|
72
73
|
"better-sqlite3": "^12.9.0",
|
|
74
|
+
"js-yaml": "^4.1.1",
|
|
73
75
|
"lucide-react": "^1.7.0",
|
|
74
76
|
"micromatch": "^4.0.8",
|
|
75
77
|
"react": "^19.2.0",
|
package/scripts/sync-plugin.mjs
CHANGED
|
@@ -725,67 +725,59 @@ function restartGateway() {
|
|
|
725
725
|
}
|
|
726
726
|
|
|
727
727
|
/**
|
|
728
|
-
* Restart Gateway on Windows
|
|
728
|
+
* Restart Gateway on Windows via schtasks.
|
|
729
|
+
* Direct task trigger bypasses OpenClaw CLI's busy-port detection which can
|
|
730
|
+
* falsely report "still busy" due to TIME_WAIT connections on the port.
|
|
729
731
|
*/
|
|
730
732
|
function restartGatewayWindows() {
|
|
731
733
|
const logPath = join(getTempDir(), 'openclaw-auto-restart.log');
|
|
732
734
|
|
|
733
735
|
try {
|
|
734
|
-
//
|
|
735
|
-
console.log('
|
|
736
|
+
// Kill existing gateway processes first (don't rely on schtasks stop)
|
|
737
|
+
console.log(' Stopping existing gateway processes...');
|
|
736
738
|
try {
|
|
737
|
-
|
|
738
|
-
// Note: Use single quotes inside -like pattern for proper escaping
|
|
739
|
-
const findCmd = "Get-Process -Name 'node' -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like '*openclaw*' } | Select-Object -ExpandProperty Id";
|
|
739
|
+
const findCmd = "Get-Process -Name 'node' -ErrorAction SilentlyContinue | Where-Object { \$_.CommandLine -like '*openclaw*' } | Select-Object -ExpandProperty Id";
|
|
740
740
|
const pids = execSync(`powershell -NoProfile -Command "${findCmd}"`, { encoding: 'utf-8' }).trim();
|
|
741
|
-
|
|
742
741
|
if (pids) {
|
|
743
|
-
console.log(` Terminating existing gateway process(es): ${pids.replace(/\n/g, ', ')}...`);
|
|
744
|
-
// Kill by PID
|
|
745
742
|
const pidList = pids.split('\n').filter(p => p.trim());
|
|
746
743
|
for (const pid of pidList) {
|
|
747
|
-
try {
|
|
748
|
-
execSync(`taskkill /PID ${pid.trim()} /F`, { stdio: 'pipe' });
|
|
749
|
-
} catch { /* ignore if process already gone */ }
|
|
744
|
+
try { execSync(`taskkill /PID ${pid.trim()} /F`, { stdio: 'pipe' }); } catch { /* ignore */ }
|
|
750
745
|
}
|
|
751
|
-
// Wait
|
|
752
|
-
execSync('timeout /t
|
|
746
|
+
// Wait for graceful shutdown
|
|
747
|
+
execSync('timeout /t 2 /nobreak > nul', { shell: true, stdio: 'ignore' });
|
|
753
748
|
}
|
|
754
749
|
} catch { /* no existing processes */ }
|
|
755
750
|
|
|
756
|
-
//
|
|
757
|
-
console.log(
|
|
751
|
+
// Trigger via schtasks — reliable, avoids CLI busy-port misdetection
|
|
752
|
+
console.log(' Starting gateway via scheduled task...');
|
|
753
|
+
execSync('schtasks /Run /TN "OpenClaw Gateway"', { stdio: 'inherit' });
|
|
758
754
|
|
|
759
|
-
//
|
|
760
|
-
const
|
|
761
|
-
const
|
|
762
|
-
execSync(`powershell -NoProfile -Command "${startCmd}"`, { stdio: 'inherit' });
|
|
763
|
-
console.log('✅ Gateway restart triggered.');
|
|
755
|
+
// Wait for gateway to start and PD plugin to register
|
|
756
|
+
const deadline = Date.now() + 20000;
|
|
757
|
+
const pollInterval = 1000;
|
|
764
758
|
|
|
765
|
-
|
|
766
|
-
|
|
759
|
+
const waitForRegistration = () => {
|
|
760
|
+
if (Date.now() > deadline) {
|
|
761
|
+
console.warn('⚠️ Gateway started but PD registration not confirmed after 20s.');
|
|
762
|
+
console.log(' Check logs at: ' + logPath);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
767
765
|
try {
|
|
768
766
|
if (existsSync(logPath)) {
|
|
769
767
|
const logs = readFileSync(logPath, 'utf-8');
|
|
770
768
|
if (logs.includes('Principles Disciple Plugin registered')) {
|
|
771
769
|
console.log('✅ SUCCESS: Principles Disciple plugin registered successfully!');
|
|
772
|
-
|
|
773
|
-
console.error('\n❌ CRITICAL: Gateway started but PD plugin FAILED to load!');
|
|
774
|
-
console.error(' Check logs at: ' + logPath);
|
|
775
|
-
process.exit(1);
|
|
776
|
-
} else {
|
|
777
|
-
console.warn('⚠️ Gateway started but PD registration not confirmed in recent logs.');
|
|
778
|
-
console.log(' Check logs at: ' + logPath);
|
|
770
|
+
return;
|
|
779
771
|
}
|
|
780
772
|
}
|
|
781
|
-
} catch
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
773
|
+
} catch { /* ignore */ }
|
|
774
|
+
setTimeout(waitForRegistration, pollInterval);
|
|
775
|
+
};
|
|
776
|
+
waitForRegistration();
|
|
785
777
|
|
|
786
778
|
} catch (error) {
|
|
787
|
-
console.error(`\n❌
|
|
788
|
-
console.error(' You may need to manually restart
|
|
779
|
+
console.error(`\n❌ Gateway restart failed: ${error.message}`);
|
|
780
|
+
console.error(' You may need to manually restart: openclaw gateway start');
|
|
789
781
|
process.exit(1);
|
|
790
782
|
}
|
|
791
783
|
}
|
package/src/core/event-log.ts
CHANGED
|
@@ -24,6 +24,14 @@ import type {
|
|
|
24
24
|
DiagnosticianReportEventData,
|
|
25
25
|
PrincipleCandidateEventData,
|
|
26
26
|
RuleEnforcedEventData,
|
|
27
|
+
// C: Nocturnal funnel events (PD-FUNNEL-2.3)
|
|
28
|
+
NocturnalDreamerCompletedEventData,
|
|
29
|
+
NocturnalArtifactPersistedEventData,
|
|
30
|
+
NocturnalCodeCandidateCreatedEventData,
|
|
31
|
+
// C: RuleHost funnel events (PD-FUNNEL-2.4)
|
|
32
|
+
RuleHostEvaluatedEventData,
|
|
33
|
+
RuleHostBlockedEventData,
|
|
34
|
+
RuleHostRequireApprovalEventData,
|
|
27
35
|
} from '../types/event-types.js';
|
|
28
36
|
import { createEmptyDailyStats } from '../types/event-types.js';
|
|
29
37
|
import { atomicWriteFileSync } from '../utils/io.js';
|
|
@@ -215,6 +223,32 @@ export class EventLog {
|
|
|
215
223
|
this.record('rule_enforced', 'matched', undefined, data);
|
|
216
224
|
}
|
|
217
225
|
|
|
226
|
+
// C: Nocturnal funnel event recorders (PD-FUNNEL-2.3)
|
|
227
|
+
recordNocturnalDreamerCompleted(data: NocturnalDreamerCompletedEventData): void {
|
|
228
|
+
this.record('nocturnal_dreamer_completed', 'completed', undefined, data);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
recordNocturnalArtifactPersisted(data: NocturnalArtifactPersistedEventData): void {
|
|
232
|
+
this.record('nocturnal_artifact_persisted', 'completed', undefined, data);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
recordNocturnalCodeCandidateCreated(data: NocturnalCodeCandidateCreatedEventData): void {
|
|
236
|
+
this.record('nocturnal_code_candidate_created', 'created', undefined, data);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// C: RuleHost funnel event recorders (PD-FUNNEL-2.4)
|
|
240
|
+
recordRuleHostEvaluated(data: RuleHostEvaluatedEventData): void {
|
|
241
|
+
this.record('rulehost_evaluated', 'evaluated', undefined, data);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
recordRuleHostBlocked(data: RuleHostBlockedEventData): void {
|
|
245
|
+
this.record('rulehost_blocked', 'blocked', undefined, data);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
recordRuleHostRequireApproval(data: RuleHostRequireApprovalEventData): void {
|
|
249
|
+
this.record('rulehost_requireApproval', 'requireApproval', undefined, data);
|
|
250
|
+
}
|
|
251
|
+
|
|
218
252
|
private record(
|
|
219
253
|
type: EventType,
|
|
220
254
|
category: EventCategory,
|
|
@@ -363,25 +397,57 @@ export class EventLog {
|
|
|
363
397
|
} else if (entry.type === 'heartbeat_diagnosis') {
|
|
364
398
|
stats.evolution.heartbeatsInjected++;
|
|
365
399
|
} else if (entry.type === 'diagnostician_report') {
|
|
366
|
-
const data = entry.data as unknown as DiagnosticianReportEventData;
|
|
367
400
|
// Backward compat: handle old events with success:boolean and new events with category:string
|
|
368
|
-
|
|
401
|
+
// Widen to Record<string, unknown> because DiagnosticianReportEventData requires
|
|
402
|
+
// category (new format) but legacy persisted events have { success: boolean }.
|
|
403
|
+
const raw = entry.data as unknown as Record<string, unknown>;
|
|
404
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'category')) {
|
|
369
405
|
// New format: category is 'success' | 'missing_json' | 'incomplete_fields'
|
|
370
|
-
|
|
406
|
+
// All three categories mean diagnosis completed and attempted to produce a report
|
|
407
|
+
const cat = raw['category'] as string;
|
|
408
|
+
if (cat === 'success' || cat === 'missing_json' || cat === 'incomplete_fields') {
|
|
371
409
|
stats.evolution.diagnosticianReportsWritten++;
|
|
372
410
|
}
|
|
373
|
-
if (
|
|
411
|
+
if (cat === 'missing_json') {
|
|
374
412
|
stats.evolution.reportsMissingJson++;
|
|
375
413
|
}
|
|
376
|
-
if (
|
|
414
|
+
if (cat === 'incomplete_fields') {
|
|
377
415
|
stats.evolution.reportsIncompleteFields++;
|
|
378
416
|
}
|
|
417
|
+
} else if (Object.prototype.hasOwnProperty.call(raw, 'success')) {
|
|
418
|
+
// Legacy format: { success: boolean }
|
|
419
|
+
// Apply agreed default semantics: treat as 'success' if true, 'missing_json' if false
|
|
420
|
+
if (raw['success']) {
|
|
421
|
+
stats.evolution.diagnosticianReportsWritten++;
|
|
422
|
+
}
|
|
423
|
+
// Note: legacy 'false' entries are not counted in any sub-counter since
|
|
424
|
+
// the old system had no such breakdown; they are invisible in sub-stats.
|
|
379
425
|
}
|
|
380
426
|
} else if (entry.type === 'principle_candidate') {
|
|
381
427
|
stats.evolution.principleCandidatesCreated++;
|
|
382
428
|
} else if (entry.type === 'rule_enforced') {
|
|
383
429
|
stats.evolution.rulesEnforced++;
|
|
384
430
|
}
|
|
431
|
+
// C: Nocturnal funnel event counters (PD-FUNNEL-2.3)
|
|
432
|
+
else if (entry.type === 'nocturnal_dreamer_completed') {
|
|
433
|
+
const data = entry.data as unknown as NocturnalDreamerCompletedEventData;
|
|
434
|
+
stats.evolution.nocturnalDreamerCompleted++;
|
|
435
|
+
if (data.chainMode === 'trinity') {
|
|
436
|
+
stats.evolution.nocturnalTrinityCompleted++;
|
|
437
|
+
}
|
|
438
|
+
} else if (entry.type === 'nocturnal_artifact_persisted') {
|
|
439
|
+
stats.evolution.nocturnalArtifactPersisted++;
|
|
440
|
+
} else if (entry.type === 'nocturnal_code_candidate_created') {
|
|
441
|
+
stats.evolution.nocturnalCodeCandidateCreated++;
|
|
442
|
+
}
|
|
443
|
+
// C: RuleHost funnel event counters (PD-FUNNEL-2.4)
|
|
444
|
+
else if (entry.type === 'rulehost_evaluated') {
|
|
445
|
+
stats.evolution.rulehostEvaluated++;
|
|
446
|
+
} else if (entry.type === 'rulehost_blocked') {
|
|
447
|
+
stats.evolution.rulehostBlocked++;
|
|
448
|
+
} else if (entry.type === 'rulehost_requireApproval') {
|
|
449
|
+
stats.evolution.rulehostRequireApproval++;
|
|
450
|
+
}
|
|
385
451
|
}
|
|
386
452
|
|
|
387
453
|
private startFlushTimer(): void {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowFunnelLoader — Loads and watches workflows.yaml as the SSOT
|
|
3
|
+
* for WORKFLOW_FUNNELS definition table.
|
|
4
|
+
*
|
|
5
|
+
* D-01: workflows.yaml is the single source of truth (SSOT).
|
|
6
|
+
* D-02: workflows.yaml lives in .state/ directory per workspace.
|
|
7
|
+
* D-03: Developers manually maintain workflows.yaml (no auto-registration).
|
|
8
|
+
* D-04: Code only reads YAML, never writes it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import yaml from 'js-yaml';
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// Types
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A single stage in a workflow funnel.
|
|
21
|
+
*/
|
|
22
|
+
export interface WorkflowStage {
|
|
23
|
+
/** Stage name within the funnel (e.g., 'dreamer_completed') */
|
|
24
|
+
name: string;
|
|
25
|
+
/** Event type string (e.g., 'nocturnal_dreamer_completed') */
|
|
26
|
+
eventType: string;
|
|
27
|
+
/** Event category (e.g., 'completed', 'created', 'blocked') */
|
|
28
|
+
eventCategory: string;
|
|
29
|
+
/** Dot-path to stats field (e.g., 'evolution.nocturnalDreamerCompleted') */
|
|
30
|
+
statsField: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A workflow funnel definition.
|
|
35
|
+
*/
|
|
36
|
+
export interface WorkflowFunnel {
|
|
37
|
+
workflowId: string;
|
|
38
|
+
stages: WorkflowStage[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Root of workflows.yaml schema.
|
|
43
|
+
*/
|
|
44
|
+
export interface WorkflowFunnelConfig {
|
|
45
|
+
version: string;
|
|
46
|
+
funnels: WorkflowFunnel[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
// WorkflowFunnelLoader
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Loads and watches workflows.yaml, building an in-memory WORKFLOW_FUNNELS table.
|
|
55
|
+
*
|
|
56
|
+
* Failure semantics (per Codex review):
|
|
57
|
+
* - Missing file: clears in-memory funnels, uses empty Map
|
|
58
|
+
* - Malformed YAML: preserves last known-good config, logs warning
|
|
59
|
+
* - Schema-invalid YAML: same as malformed YAML
|
|
60
|
+
*
|
|
61
|
+
* Usage:
|
|
62
|
+
* const loader = new WorkflowFunnelLoader(stateDir);
|
|
63
|
+
* const funnels = loader.getAllFunnels(); // Map<string, WorkflowStage[]>
|
|
64
|
+
* loader.watch(); // Enable hot reload
|
|
65
|
+
*/
|
|
66
|
+
export class WorkflowFunnelLoader {
|
|
67
|
+
/** In-memory WORKFLOW_FUNNELS table: workflowId -> stages */
|
|
68
|
+
private readonly funnels = new Map<string, WorkflowStage[]>();
|
|
69
|
+
|
|
70
|
+
private readonly configPath: string;
|
|
71
|
+
|
|
72
|
+
/** fs.watch() handle for cleanup */
|
|
73
|
+
private watchHandle?: fs.FSWatcher;
|
|
74
|
+
|
|
75
|
+
constructor(stateDir: string) {
|
|
76
|
+
// D-02: workflows.yaml in .state/ directory
|
|
77
|
+
this.configPath = path.join(stateDir, 'workflows.yaml');
|
|
78
|
+
this.load();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Load (or reload) workflows.yaml from disk.
|
|
83
|
+
* On parse/validation failure, preserves the last known-good config.
|
|
84
|
+
* On missing file, clears to empty.
|
|
85
|
+
*/
|
|
86
|
+
load(): void {
|
|
87
|
+
if (!fs.existsSync(this.configPath)) {
|
|
88
|
+
this.funnels.clear();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const content = fs.readFileSync(this.configPath, 'utf-8');
|
|
94
|
+
// Use safe load — no arbitrary code execution
|
|
95
|
+
const config = yaml.load(content, { schema: yaml.DEFAULT_SCHEMA }) as WorkflowFunnelConfig;
|
|
96
|
+
|
|
97
|
+
// Validate top-level structure
|
|
98
|
+
if (!config || typeof config.version !== 'string' || !Array.isArray(config.funnels)) {
|
|
99
|
+
console.warn(`[WorkflowFunnelLoader] workflows.yaml validation failed: missing version or funnels array. Preserving last valid config.`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Rebuild funnels map
|
|
104
|
+
const newFunnels = new Map<string, WorkflowStage[]>();
|
|
105
|
+
for (const funnel of config.funnels) {
|
|
106
|
+
if (funnel?.workflowId && typeof funnel.workflowId === 'string' && Array.isArray(funnel.stages)) {
|
|
107
|
+
newFunnels.set(funnel.workflowId, funnel.stages);
|
|
108
|
+
} else {
|
|
109
|
+
console.warn(`[WorkflowFunnelLoader] Skipping invalid funnel entry: missing workflowId or stages.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Atomic replace: only commit if entire parse/validation succeeded
|
|
114
|
+
this.funnels.clear();
|
|
115
|
+
for (const [k, v] of newFunnels) {
|
|
116
|
+
this.funnels.set(k, v);
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
// Best-effort: preserve last known-good config on parse error
|
|
120
|
+
console.warn(`[WorkflowFunnelLoader] Failed to parse workflows.yaml: ${String(err)}. Preserving last valid config.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Start watching workflows.yaml for changes.
|
|
126
|
+
* Calls load() automatically when the file changes.
|
|
127
|
+
*/
|
|
128
|
+
watch(): void {
|
|
129
|
+
// Debounce: only re-read after file write settles (100ms)
|
|
130
|
+
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
131
|
+
this.watchHandle = fs.watch(this.configPath, (eventType) => {
|
|
132
|
+
if (eventType !== 'change') return;
|
|
133
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
134
|
+
debounceTimer = setTimeout(() => {
|
|
135
|
+
this.load();
|
|
136
|
+
}, 100);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Stop watching and clean up the FSWatcher.
|
|
142
|
+
*/
|
|
143
|
+
dispose(): void {
|
|
144
|
+
if (this.watchHandle) {
|
|
145
|
+
this.watchHandle.close();
|
|
146
|
+
this.watchHandle = undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get all stages for a workflow.
|
|
152
|
+
*/
|
|
153
|
+
getStages(workflowId: string): WorkflowStage[] {
|
|
154
|
+
return this.funnels.get(workflowId) ?? [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get the full WORKFLOW_FUNNELS table.
|
|
159
|
+
*/
|
|
160
|
+
getAllFunnels(): Map<string, WorkflowStage[]> {
|
|
161
|
+
return new Map(this.funnels);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get the config file path (for testing/debugging).
|
|
166
|
+
*/
|
|
167
|
+
getConfigPath(): string {
|
|
168
|
+
return this.configPath;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* PURPOSE: Provide ONE authoritative implementation for gate block persistence.
|
|
5
5
|
*
|
|
6
|
-
* All gate
|
|
6
|
+
* All gate sources (rule-host) must use this
|
|
7
7
|
* helper to ensure consistent block tracking, event logging, and retry behavior.
|
|
8
8
|
*
|
|
9
9
|
* This eliminates the "multi-truth source" problem where different modules
|