principles-disciple 1.79.0 → 1.80.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.79.0",
5
+ "version": "1.80.0",
6
6
  "activation": {
7
7
  "onCapabilities": [
8
8
  "hook"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.79.0",
3
+ "version": "1.80.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -0,0 +1,198 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ checkSurfaceGuard,
4
+ isSurfaceEnabled,
5
+ guardHook,
6
+ guardService,
7
+ getSurfaceIdForHook,
8
+ getSurfaceIdForService,
9
+ } from '../../src/core/surface-guard.js';
10
+ import { PLUGIN_SURFACE_REGISTRY } from '@principles/core/runtime-v2';
11
+ import type { OpenClawPluginService } from '../../src/openclaw-sdk.js';
12
+
13
+ describe('surface-guard', () => {
14
+ describe('getSurfaceIdForHook', () => {
15
+ it('generates correct surface id without label', () => {
16
+ expect(getSurfaceIdForHook('before_tool_call')).toBe('hook:before_tool_call');
17
+ });
18
+
19
+ it('generates correct surface id with label', () => {
20
+ expect(getSurfaceIdForHook('after_tool_call', 'trajectory')).toBe('hook:after_tool_call.trajectory');
21
+ });
22
+ });
23
+
24
+ describe('getSurfaceIdForService', () => {
25
+ it('generates correct surface id for service', () => {
26
+ expect(getSurfaceIdForService('evolution-worker')).toBe('service:evolution-worker');
27
+ });
28
+ });
29
+
30
+ describe('checkSurfaceGuard', () => {
31
+ it('returns passed=true when registry is valid', () => {
32
+ const result = checkSurfaceGuard();
33
+ expect(result.passed).toBe(true);
34
+ expect(result.violations).toEqual([]);
35
+ });
36
+
37
+ it('includes enabled core surfaces', () => {
38
+ const result = checkSurfaceGuard();
39
+ expect(result.enabledCoreSurfaces.length).toBeGreaterThan(0);
40
+ for (const surfaceId of result.enabledCoreSurfaces) {
41
+ const entry = PLUGIN_SURFACE_REGISTRY.find(s => s.id === surfaceId);
42
+ expect(entry).toBeDefined();
43
+ expect(entry?.category).toBe('core');
44
+ }
45
+ });
46
+
47
+ it('includes disabled non-core surfaces', () => {
48
+ const result = checkSurfaceGuard();
49
+ expect(result.disabledNonCoreSurfaces.length).toBeGreaterThan(0);
50
+ for (const surfaceId of result.disabledNonCoreSurfaces) {
51
+ const entry = PLUGIN_SURFACE_REGISTRY.find(s => s.id === surfaceId);
52
+ expect(entry).toBeDefined();
53
+ expect(entry?.category).not.toBe('core');
54
+ expect(entry?.enabledByDefault).toBe(false);
55
+ }
56
+ });
57
+
58
+ it('returns violations when non-core surface is enabledByDefault', () => {
59
+ const result = checkSurfaceGuard();
60
+ const nonCoreEnabled = PLUGIN_SURFACE_REGISTRY.filter(
61
+ s => s.category !== 'core' && s.enabledByDefault,
62
+ );
63
+ if (nonCoreEnabled.length > 0) {
64
+ expect(result.violations.length).toBeGreaterThan(0);
65
+ }
66
+ });
67
+ });
68
+
69
+ describe('isSurfaceEnabled', () => {
70
+ it('returns enabled=true for core surface without override', () => {
71
+ const result = isSurfaceEnabled('hook:before_prompt_build');
72
+ expect(result.enabled).toBe(true);
73
+ expect(result.reason).toBeUndefined();
74
+ });
75
+
76
+ it('returns enabled=false for quiet surface without override', () => {
77
+ const result = isSurfaceEnabled('hook:after_tool_call.trajectory');
78
+ expect(result.enabled).toBe(false);
79
+ expect(result.reason).toBeDefined();
80
+ });
81
+
82
+ it('returns reason when surface not found', () => {
83
+ const result = isSurfaceEnabled('hook:nonexistent_hook');
84
+ expect(result.enabled).toBe(false);
85
+ expect(result.reason).toContain('not found in registry');
86
+ });
87
+
88
+ it('allows override for quiet surface', () => {
89
+ const result = isSurfaceEnabled('hook:after_tool_call.trajectory', {
90
+ 'hook:after_tool_call.trajectory': true,
91
+ });
92
+ expect(result.enabled).toBe(true);
93
+ });
94
+
95
+ it('ignores non-boolean override', () => {
96
+ const result = isSurfaceEnabled('hook:before_prompt_build', {
97
+ 'hook:before_prompt_build': 'yes' as unknown as boolean,
98
+ });
99
+ expect(result.enabled).toBe(true);
100
+ });
101
+
102
+ it('cannot disable core surface', () => {
103
+ const result = isSurfaceEnabled('hook:before_prompt_build', {
104
+ 'hook:before_prompt_build': false,
105
+ });
106
+ expect(result.enabled).toBe(true);
107
+ expect(result.reason).toContain('core');
108
+ });
109
+
110
+ it('returns disabledReason for disabled surface', () => {
111
+ const result = isSurfaceEnabled('service:evolution-worker');
112
+ expect(result.enabled).toBe(false);
113
+ expect(result.reason).toContain('evolution_worker');
114
+ });
115
+ });
116
+
117
+ describe('guardHook', () => {
118
+ beforeEach(() => {
119
+ vi.clearAllMocks();
120
+ });
121
+
122
+ it('returns original handler for enabled surface', () => {
123
+ const mockHandler = vi.fn().mockReturnValue('result');
124
+ const guarded = guardHook('hook:before_prompt_build', undefined, mockHandler);
125
+ const result = guarded({}, {});
126
+ expect(mockHandler).toHaveBeenCalled();
127
+ expect(result).toBe('result');
128
+ });
129
+
130
+ it('returns no-op for disabled surface without logger', () => {
131
+ const mockHandler = vi.fn();
132
+ const guarded = guardHook('hook:after_tool_call.trajectory', undefined, mockHandler);
133
+ const result = guarded({}, {});
134
+ expect(mockHandler).not.toHaveBeenCalled();
135
+ expect(result).toBeUndefined();
136
+ });
137
+
138
+ it('logs when surface is disabled with logger', () => {
139
+ const mockLogger = { info: vi.fn(), debug: vi.fn() };
140
+ const mockHandler = vi.fn();
141
+ const guarded = guardHook('hook:after_tool_call.trajectory', mockLogger, mockHandler);
142
+ guarded({}, {});
143
+ expect(mockLogger.info).toHaveBeenCalled();
144
+ expect(mockHandler).not.toHaveBeenCalled();
145
+ });
146
+
147
+ it('does not log for enabled surface', () => {
148
+ const mockLogger = { info: vi.fn(), debug: vi.fn() };
149
+ const mockHandler = vi.fn();
150
+ const guarded = guardHook('hook:before_prompt_build', mockLogger, mockHandler);
151
+ guarded({}, {});
152
+ expect(mockLogger.info).not.toHaveBeenCalled();
153
+ });
154
+
155
+ it('guards unknown surface with not-found reason', () => {
156
+ const mockLogger = { info: vi.fn(), debug: vi.fn() };
157
+ const guarded = guardHook('hook:unknown_hook', mockLogger, vi.fn());
158
+ guarded({}, {});
159
+ expect(mockLogger.info).toHaveBeenCalledWith(
160
+ expect.stringContaining('not found in registry'),
161
+ );
162
+ });
163
+ });
164
+
165
+ describe('guardService', () => {
166
+ beforeEach(() => {
167
+ vi.clearAllMocks();
168
+ });
169
+
170
+ it('returns original service for enabled surface', () => {
171
+ const mockService: OpenClawPluginService = { id: 'test-service' };
172
+ const result = guardService('hook:before_prompt_build', mockService);
173
+ expect(result).toBe(mockService);
174
+ });
175
+
176
+ it('returns null for disabled surface without logger', () => {
177
+ const mockService: OpenClawPluginService = { id: 'test-service' };
178
+ const result = guardService('hook:after_tool_call.trajectory', mockService);
179
+ expect(result).toBeNull();
180
+ });
181
+
182
+ it('logs when surface is disabled with logger', () => {
183
+ const mockLogger = { info: vi.fn(), debug: vi.fn() };
184
+ const mockService: OpenClawPluginService = { id: 'test-service' };
185
+ const result = guardService('hook:after_tool_call.trajectory', mockService, mockLogger);
186
+ expect(result).toBeNull();
187
+ expect(mockLogger.info).toHaveBeenCalledWith(
188
+ expect.stringContaining('SKIP service'),
189
+ );
190
+ });
191
+
192
+ it('returns null for unknown surface', () => {
193
+ const mockService: OpenClawPluginService = { id: 'test-service' };
194
+ const result = guardService('service:nonexistent', mockService);
195
+ expect(result).toBeNull();
196
+ });
197
+ });
198
+ });
@@ -0,0 +1,201 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import type { Dirent } from 'fs';
5
+ import type { OpenClawPluginApi } from '../../src/openclaw-sdk.js';
6
+
7
+ const mockFs = {
8
+ existsSync: vi.fn(),
9
+ readFileSync: vi.fn(),
10
+ writeFileSync: vi.fn(),
11
+ readdirSync: vi.fn(),
12
+ };
13
+
14
+ vi.mock('fs', () => mockFs);
15
+
16
+ const WORKSPACE_GUIDANCE_MIGRATOR_PATH = '../../src/core/workspace-guidance-migrator.js';
17
+
18
+ describe('workspace-guidance-migrator', () => {
19
+ let migrateStaleWorkspaceGuidance: (api: OpenClawPluginApi, workspaceDir: string) => {
20
+ migratedFiles: string[];
21
+ skippedFiles: string[];
22
+ errors: { file: string; error: string }[];
23
+ };
24
+
25
+ const mockLogger = {
26
+ info: vi.fn(),
27
+ warn: vi.fn(),
28
+ error: vi.fn(),
29
+ };
30
+
31
+ const mockApi = {
32
+ logger: mockLogger,
33
+ } as unknown as OpenClawPluginApi;
34
+
35
+ beforeEach(async () => {
36
+ vi.clearAllMocks();
37
+ vi.resetModules();
38
+
39
+ mockFs.existsSync.mockReturnValue(true);
40
+ mockFs.readFileSync.mockReturnValue('');
41
+ mockFs.writeFileSync.mockReturnValue(undefined);
42
+ mockFs.readdirSync.mockReturnValue([]);
43
+
44
+ const module = await import(WORKSPACE_GUIDANCE_MIGRATOR_PATH);
45
+ migrateStaleWorkspaceGuidance = module.migrateStaleWorkspaceGuidance;
46
+ });
47
+
48
+ afterEach(() => {
49
+ vi.restoreAllMocks();
50
+ });
51
+
52
+ describe('migrateStaleWorkspaceGuidance', () => {
53
+ it('skips files that do not exist', () => {
54
+ mockFs.existsSync.mockReturnValue(false);
55
+
56
+ const result = migrateStaleWorkspaceGuidance(mockApi, '/workspace');
57
+
58
+ expect(result.migratedFiles).toEqual([]);
59
+ expect(result.skippedFiles).toEqual([]);
60
+ expect(result.errors).toEqual([]);
61
+ });
62
+
63
+ it('skips files with no stale guidance', () => {
64
+ mockFs.existsSync.mockReturnValue(true);
65
+ mockFs.readFileSync.mockReturnValue('# Clean AGENTS.md\nNo stale references here.');
66
+
67
+ const result = migrateStaleWorkspaceGuidance(mockApi, '/workspace');
68
+
69
+ expect(result.migratedFiles).toEqual([]);
70
+ expect(result.skippedFiles.length).toBeGreaterThan(0);
71
+ expect(result.errors).toEqual([]);
72
+ });
73
+
74
+ it('migrates AGENTS.md with stale guidance', () => {
75
+ mockFs.existsSync.mockReturnValue(true);
76
+ mockFs.readFileSync.mockReturnValue(
77
+ '# Agent Instructions\nPhysical interception ensures safety.',
78
+ );
79
+
80
+ const result = migrateStaleWorkspaceGuidance(mockApi, '/workspace');
81
+
82
+ expect(result.migratedFiles.some(f => f.includes('AGENTS.md'))).toBe(true);
83
+ expect(result.skippedFiles.some(f => f.includes('MEMORY.md'))).toBe(true);
84
+ });
85
+
86
+ it('creates backup before migration', () => {
87
+ mockFs.existsSync.mockReturnValue(true);
88
+ mockFs.readFileSync.mockReturnValue(
89
+ '# Agent Instructions\nPhysical interception ensures safety.',
90
+ );
91
+
92
+ migrateStaleWorkspaceGuidance(mockApi, '/workspace');
93
+
94
+ const backupCalls = mockFs.writeFileSync.mock.calls.filter(
95
+ (call: unknown[]) => String(call[0]).includes('.pre-pri286.bak'),
96
+ );
97
+ expect(backupCalls.length).toBeGreaterThan(0);
98
+ });
99
+
100
+ it('logs migration progress', () => {
101
+ mockFs.existsSync.mockReturnValue(true);
102
+ mockFs.readFileSync.mockReturnValue(
103
+ '# Agent Instructions\nPhysical interception ensures safety.',
104
+ );
105
+
106
+ migrateStaleWorkspaceGuidance(mockApi, '/workspace');
107
+
108
+ expect(mockLogger.info).toHaveBeenCalledWith(
109
+ expect.stringContaining('[PD:GuidanceMigration]'),
110
+ );
111
+ });
112
+
113
+ it('handles read errors gracefully', () => {
114
+ mockFs.existsSync.mockReturnValue(true);
115
+ mockFs.readFileSync.mockImplementation(() => {
116
+ throw new Error('Read error');
117
+ });
118
+
119
+ const result = migrateStaleWorkspaceGuidance(mockApi, '/workspace');
120
+
121
+ expect(result.errors.length).toBeGreaterThan(0);
122
+ expect(result.errors[0].error).toContain('Failed to read file content');
123
+ });
124
+
125
+ it('handles write errors and restores original', () => {
126
+ const originalContent = '# Agent Instructions\nPhysical interception ensures safety.';
127
+ mockFs.existsSync.mockReturnValue(true);
128
+ mockFs.readFileSync.mockReturnValue(originalContent);
129
+
130
+ let callCount = 0;
131
+ mockFs.writeFileSync.mockImplementation((path: string, content: string) => {
132
+ callCount++;
133
+ if (path.includes('.pre-pri286.bak')) return;
134
+ if (callCount === 2) {
135
+ expect(content).toBe(originalContent);
136
+ return;
137
+ }
138
+ throw new Error('Write error');
139
+ });
140
+
141
+ const result = migrateStaleWorkspaceGuidance(mockApi, '/workspace');
142
+
143
+ expect(result.errors.length).toBeGreaterThan(0);
144
+ expect(callCount).toBeGreaterThanOrEqual(2);
145
+ });
146
+
147
+ it('skips non-guidance files', () => {
148
+ mockFs.existsSync.mockReturnValue(true);
149
+ mockFs.readFileSync.mockReturnValue('# Random Content\nNo guidance here.');
150
+
151
+ const result = migrateStaleWorkspaceGuidance(mockApi, '/workspace');
152
+
153
+ expect(result.migratedFiles).toEqual([]);
154
+ });
155
+
156
+ it('discovers skill files in .principles/skills directory', () => {
157
+ mockFs.existsSync.mockImplementation((p: string) => {
158
+ if (String(p).includes('.principles/skills')) return true;
159
+ return false;
160
+ });
161
+ mockFs.readdirSync.mockReturnValue([
162
+ { isDirectory: () => true, name: 'admin' },
163
+ { isDirectory: () => true, name: 'reflection' },
164
+ ] as Dirent[]);
165
+ mockFs.readFileSync.mockReturnValue(
166
+ 'Ensure `PLAN.md` contains `## Target Files` heading.',
167
+ );
168
+
169
+ const result = migrateStaleWorkspaceGuidance(mockApi, '/workspace');
170
+
171
+ expect(result.migratedFiles.length).toBeGreaterThan(0);
172
+ });
173
+
174
+ it('handles empty workspace directory', () => {
175
+ mockFs.existsSync.mockReturnValue(false);
176
+
177
+ const result = migrateStaleWorkspaceGuidance(mockApi, '/workspace');
178
+
179
+ expect(result.migratedFiles).toEqual([]);
180
+ expect(result.skippedFiles).toEqual([]);
181
+ expect(result.errors).toEqual([]);
182
+ });
183
+
184
+ it('handles skills directory read error gracefully', () => {
185
+ mockFs.existsSync.mockImplementation((p: string) => {
186
+ if (String(p).includes('.principles/skills')) return true;
187
+ return false;
188
+ });
189
+ mockFs.readdirSync.mockImplementation(() => {
190
+ throw new Error('Directory read error');
191
+ });
192
+ mockFs.readFileSync.mockReturnValue(
193
+ '# Agent Instructions\nPhysical interception ensures safety.',
194
+ );
195
+
196
+ const result = migrateStaleWorkspaceGuidance(mockApi, '/workspace');
197
+
198
+ expect(result.errors.length).toBeGreaterThan(0);
199
+ });
200
+ });
201
+ });