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.
@@ -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.61.0",
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": "b300ef0761c5",
80
- "bundleMd5": "3c16a5198f3559b097d028b6e987cf5b",
81
- "builtAt": "2026-04-18T11:02:10.525Z"
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.61.0",
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",
@@ -725,67 +725,59 @@ function restartGateway() {
725
725
  }
726
726
 
727
727
  /**
728
- * Restart Gateway on Windows using PowerShell.
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
- // Step 1: Find and terminate existing gateway processes
735
- console.log(' Looking for existing gateway processes...');
736
+ // Kill existing gateway processes first (don't rely on schtasks stop)
737
+ console.log(' Stopping existing gateway processes...');
736
738
  try {
737
- // PowerShell command to find and kill openclaw gateway processes
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 a moment for process to terminate
752
- execSync('timeout /t 3 /nobreak > nul', { shell: true, stdio: 'ignore' });
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
- // Step 2: Start new gateway process in background
757
- console.log(` Starting new gateway (logs: ${logPath})...`);
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
- // Use openclaw CLI to start gateway (more reliable than direct node invocation)
760
- const gatewayCmd = join(getHomeDir(), '.openclaw', 'gateway.cmd');
761
- const startCmd = `Start-Process -FilePath 'cmd.exe' -ArgumentList '/c ${gatewayCmd}' -WindowStyle Hidden -RedirectStandardOutput '${logPath}' -RedirectStandardError '${join(getTempDir(), 'openclaw-auto-restart.err')}'`;
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
- // Step 3: Wait and verify
766
- setTimeout(() => {
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
- } else if (logs.includes('failed to load') || logs.includes('Error: Cannot find module')) {
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 (e) {
782
- console.warn(`⚠️ Post-restart verification skipped: ${e.message}`);
783
- }
784
- }, 8000);
773
+ } catch { /* ignore */ }
774
+ setTimeout(waitForRegistration, pollInterval);
775
+ };
776
+ waitForRegistration();
785
777
 
786
778
  } catch (error) {
787
- console.error(`\n❌ Failed to restart gateway: ${error.message}`);
788
- console.error(' You may need to manually restart OpenClaw Gateway.');
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
  }
@@ -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
- if ('category' in data) {
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
- if (data.category === 'success' || data.category === 'incomplete_fields') {
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 (data.category === 'missing_json') {
411
+ if (cat === 'missing_json') {
374
412
  stats.evolution.reportsMissingJson++;
375
413
  }
376
- if (data.category === 'incomplete_fields') {
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 modules (progressive-trust-gate, gfi-gate, etc.) must use this
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