principles-disciple 1.32.0 → 1.34.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.
Files changed (37) hide show
  1. package/openclaw.plugin.json +4 -4
  2. package/package.json +1 -1
  3. package/src/core/correction-cue-learner.ts +203 -0
  4. package/src/core/correction-types.ts +88 -0
  5. package/src/core/evolution-logger.ts +3 -3
  6. package/src/core/init.ts +67 -0
  7. package/src/service/correction-observer-types.ts +58 -0
  8. package/src/service/correction-observer-workflow-manager.ts +218 -0
  9. package/src/service/evolution-worker.ts +172 -146
  10. package/src/service/nocturnal-service.ts +4 -1
  11. package/src/service/subagent-workflow/index.ts +14 -0
  12. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +3 -1
  13. package/tests/service/evolution-worker.nocturnal.test.ts +14 -1
  14. package/tests/service/evolution-worker.timeout.test.ts +350 -0
  15. package/tests/commands/implementation-lifecycle.test.ts +0 -362
  16. package/tests/core/detection-funnel.test.ts +0 -63
  17. package/tests/core/evolution-e2e.test.ts +0 -58
  18. package/tests/core/evolution-engine-gate-integration.test.ts +0 -543
  19. package/tests/core/evolution-engine.test.ts +0 -562
  20. package/tests/core/evolution-reducer.test.ts +0 -180
  21. package/tests/core/evolution-user-stories.e2e.test.ts +0 -249
  22. package/tests/core/local-worker-routing.test.ts +0 -757
  23. package/tests/core/rule-host.test.ts +0 -389
  24. package/tests/core/trajectory-correction-pain.test.ts +0 -180
  25. package/tests/hooks/gate-edit-verification.test.ts +0 -435
  26. package/tests/hooks/llm.test.ts +0 -308
  27. package/tests/hooks/progressive-trust-gate.test.ts +0 -277
  28. package/tests/hooks/prompt.test.ts +0 -1473
  29. package/tests/index.integration.test.ts +0 -179
  30. package/tests/index.shadow-routing.integration.test.ts +0 -140
  31. package/tests/service/evolution-worker.test.ts +0 -462
  32. package/tests/service/nocturnal-service.test.ts +0 -577
  33. package/tests/service/nocturnal-workflow-manager.test.ts +0 -441
  34. package/tests/tools/critique-prompt.test.ts +0 -260
  35. package/tests/tools/deep-reflect.test.ts +0 -232
  36. package/tests/tools/model-index.test.ts +0 -246
  37. package/tests/ui/app.test.tsx +0 -114
@@ -1,362 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import * as fs from 'fs';
3
- import * as os from 'os';
4
- import * as path from 'path';
5
- import { WorkspaceContext } from '../../src/core/workspace-context.js';
6
- import { createImplementationAssetDir, getImplementationAssetRoot } from '../../src/core/code-implementation-storage.js';
7
- import {
8
- loadLedger,
9
- saveLedger,
10
- type LedgerPrinciple,
11
- type LedgerRule,
12
- } from '../../src/core/principle-tree-ledger.js';
13
- import { handlePromoteImplCommand } from '../../src/commands/promote-impl.js';
14
- import { handleDisableImplCommand } from '../../src/commands/disable-impl.js';
15
- import { handleArchiveImplCommand } from '../../src/commands/archive-impl.js';
16
- import { handleRollbackImplCommand } from '../../src/commands/rollback-impl.js';
17
- import type { PluginCommandContext } from '../../src/openclaw-sdk.js';
18
- import type { Implementation } from '../../src/types/principle-tree-schema.js';
19
- import { TrajectoryDatabase, TrajectoryRegistry } from '../../src/core/trajectory.js';
20
- import { registerSample } from '../../src/core/nocturnal-dataset.js';
21
- import { safeRmDir } from '../test-utils.js';
22
-
23
- function makePrinciple(): LedgerPrinciple {
24
- return {
25
- id: 'P-1',
26
- version: 1,
27
- text: 'Prefer explicit principle-backed implementations',
28
- triggerPattern: 'principle',
29
- action: 'use implementations safely',
30
- status: 'active',
31
- priority: 'P1',
32
- scope: 'general',
33
- evaluability: 'deterministic',
34
- valueScore: 0,
35
- adherenceRate: 0,
36
- painPreventedCount: 0,
37
- derivedFromPainIds: [],
38
- ruleIds: ['R-1'],
39
- conflictsWithPrincipleIds: [],
40
- createdAt: '2026-04-08T00:00:00.000Z',
41
- updatedAt: '2026-04-08T00:00:00.000Z',
42
- };
43
- }
44
-
45
- function makeRule(implementationIds: string[]): LedgerRule {
46
- return {
47
- id: 'R-1',
48
- version: 1,
49
- name: 'Guard implementation lifecycle',
50
- description: 'Tracks implementation state transitions',
51
- type: 'gate',
52
- triggerCondition: 'lifecycle',
53
- enforcement: 'warn',
54
- action: 'record state safely',
55
- principleId: 'P-1',
56
- status: 'implemented',
57
- coverageRate: 0,
58
- falsePositiveRate: 0,
59
- implementationPath: undefined,
60
- testPath: undefined,
61
- createdAt: '2026-04-08T00:00:00.000Z',
62
- updatedAt: '2026-04-08T00:00:00.000Z',
63
- implementationIds,
64
- };
65
- }
66
-
67
- function makeImplementation(
68
- id: string,
69
- lifecycleState: Implementation['lifecycleState'],
70
- overrides: Partial<Implementation> = {}
71
- ): Implementation {
72
- return {
73
- id,
74
- ruleId: 'R-1',
75
- type: 'code',
76
- path: path.join('virtual', `${id}.js`),
77
- version: '1.0.0',
78
- coversCondition: 'lifecycle',
79
- coveragePercentage: 100,
80
- lifecycleState,
81
- createdAt: '2026-04-08T00:00:00.000Z',
82
- updatedAt: '2026-04-08T00:00:00.000Z',
83
- ...overrides,
84
- };
85
- }
86
-
87
- function makeCommandContext(workspaceDir: string, args: string): PluginCommandContext {
88
- return {
89
- channel: 'test',
90
- isAuthorizedSender: true,
91
- commandBody: args,
92
- args,
93
- config: { workspaceDir, language: 'en' },
94
- workspaceDir,
95
- sessionId: 'session-1',
96
- };
97
- }
98
-
99
- function writePassingReplayReport(stateDir: string, implementationId: string): void {
100
- const assetRoot = getImplementationAssetRoot(stateDir, implementationId);
101
- const reportPath = path.join(assetRoot, 'replays', '2026-04-08T00-00-00.000Z.json');
102
- fs.writeFileSync(
103
- reportPath,
104
- JSON.stringify(
105
- {
106
- overallDecision: 'pass',
107
- replayResults: {
108
- painNegative: { total: 1, passed: 1, failed: 0, details: [] },
109
- successPositive: { total: 1, passed: 1, failed: 0, details: [] },
110
- principleAnchor: { total: 1, passed: 1, failed: 0, details: [] },
111
- },
112
- blockers: [],
113
- generatedAt: '2026-04-08T00:00:00.000Z',
114
- implementationId,
115
- sampleFingerprints: ['fp-1'],
116
- },
117
- null,
118
- 2
119
- ),
120
- 'utf-8'
121
- );
122
- }
123
-
124
- describe('implementation lifecycle commands', () => {
125
- let tempDir: string;
126
- let workspaceDir: string;
127
- let stateDir: string;
128
- let trajectory: TrajectoryDatabase;
129
-
130
- beforeEach(() => {
131
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-impl-cmd-'));
132
- workspaceDir = path.join(tempDir, 'workspace');
133
- stateDir = path.join(tempDir, '.state');
134
- fs.mkdirSync(workspaceDir, { recursive: true });
135
- fs.mkdirSync(stateDir, { recursive: true });
136
- fs.writeFileSync(
137
- path.join(workspaceDir, 'PROFILE.json'),
138
- JSON.stringify({ risk_paths: ['src/**'] }, null, 2),
139
- 'utf-8',
140
- );
141
- trajectory = new TrajectoryDatabase({ workspaceDir });
142
-
143
- WorkspaceContext.clearCache();
144
- const workspaceContext = WorkspaceContext.fromHookContext({ workspaceDir, stateDir });
145
- vi.spyOn(WorkspaceContext, 'fromHookContext').mockReturnValue(workspaceContext);
146
- });
147
-
148
- afterEach(() => {
149
- try {
150
- trajectory.dispose();
151
- } catch {
152
- // Best effort cleanup.
153
- }
154
- try {
155
- TrajectoryRegistry.dispose(workspaceDir);
156
- } catch {
157
- // Best effort cleanup.
158
- }
159
- vi.restoreAllMocks();
160
- WorkspaceContext.clearCache();
161
- safeRmDir(tempDir);
162
- });
163
-
164
- function seedLedger(implementations: Implementation[]): void {
165
- saveLedger(stateDir, {
166
- trainingStore: {
167
- 'P-1': {
168
- principleId: 'P-1',
169
- evaluability: 'deterministic',
170
- applicableOpportunityCount: 1,
171
- observedViolationCount: 0,
172
- complianceRate: 1,
173
- violationTrend: 0,
174
- generatedSampleCount: 0,
175
- approvedSampleCount: 0,
176
- includedTrainRunIds: [],
177
- deployedCheckpointIds: [],
178
- internalizationStatus: 'internalized',
179
- },
180
- },
181
- tree: {
182
- principles: {
183
- 'P-1': makePrinciple(),
184
- },
185
- rules: {
186
- 'R-1': makeRule(implementations.map((impl) => impl.id)),
187
- },
188
- implementations: Object.fromEntries(implementations.map((impl) => [impl.id, impl])),
189
- metrics: {},
190
- lastUpdated: '2026-04-08T00:00:00.000Z',
191
- },
192
- });
193
- }
194
-
195
- function seedReplaySample(sessionId: string, artifactId: string): void {
196
- trajectory.recordSession({
197
- sessionId,
198
- startedAt: '2026-04-08T00:00:00.000Z',
199
- });
200
- trajectory.recordToolCall({
201
- sessionId,
202
- toolName: 'write',
203
- outcome: 'blocked',
204
- errorMessage: 'risky write requires approval',
205
- paramsJson: { filePath: 'src/app.ts' },
206
- createdAt: '2026-04-08T00:01:00.000Z',
207
- });
208
- trajectory.recordPainEvent({
209
- sessionId,
210
- source: 'gate',
211
- score: 75,
212
- reason: 'risky write requires approval',
213
- createdAt: '2026-04-08T00:01:01.000Z',
214
- });
215
- trajectory.recordGateBlock({
216
- sessionId,
217
- toolName: 'write',
218
- filePath: 'src/app.ts',
219
- reason: 'risky write requires approval',
220
- planStatus: 'NONE',
221
- createdAt: '2026-04-08T00:01:02.000Z',
222
- });
223
-
224
- const artifactPath = path.join(stateDir, 'nocturnal', 'samples', `${artifactId}.json`);
225
- fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
226
- fs.writeFileSync(
227
- artifactPath,
228
- JSON.stringify(
229
- {
230
- artifactId,
231
- sessionId,
232
- principleId: 'P-1',
233
- sourceSnapshotRef: `snapshot-${sessionId}`,
234
- badDecision: 'Retried the write without checking approval requirements',
235
- betterDecision: 'Check the approval requirements before writing',
236
- rationale: 'Riskiest paths should not be written without a plan',
237
- createdAt: '2026-04-08T00:02:00.000Z',
238
- },
239
- null,
240
- 2,
241
- ),
242
- 'utf-8',
243
- );
244
-
245
- registerSample(
246
- workspaceDir,
247
- {
248
- artifactId,
249
- sessionId,
250
- principleId: 'P-1',
251
- sourceSnapshotRef: `snapshot-${sessionId}`,
252
- badDecision: 'Retried the write without checking approval requirements',
253
- betterDecision: 'Check the approval requirements before writing',
254
- rationale: 'Riskiest paths should not be written without a plan',
255
- createdAt: '2026-04-08T00:02:00.000Z',
256
- },
257
- artifactPath,
258
- null,
259
- 'pain-negative',
260
- );
261
- }
262
-
263
- it('promote preserves the legacy training store while activating the candidate', () => {
264
- seedLedger([makeImplementation('IMPL-CAND', 'candidate')]);
265
- createImplementationAssetDir(stateDir, 'IMPL-CAND', '1.0.0');
266
- writePassingReplayReport(stateDir, 'IMPL-CAND');
267
-
268
- const result = handlePromoteImplCommand(makeCommandContext(workspaceDir, 'IMPL-CAND'));
269
-
270
- expect(result.text).toContain('Implementation promoted');
271
- expect(loadLedger(stateDir).tree.implementations['IMPL-CAND'].lifecycleState).toBe('active');
272
-
273
- const raw = JSON.parse(
274
- fs.readFileSync(path.join(stateDir, 'principle_training_state.json'), 'utf-8')
275
- ) as Record<string, unknown>;
276
- expect(raw['P-1']).toBeDefined();
277
- expect(raw._tree).toBeDefined();
278
- });
279
-
280
- it('eval generates a replay report for a candidate implementation before promotion', () => {
281
- seedLedger([makeImplementation('IMPL-CAND', 'candidate')]);
282
- createImplementationAssetDir(stateDir, 'IMPL-CAND', '1.0.0', {
283
- entrySource: [
284
- 'export const meta = {',
285
- ' name: "risky-write-guard",',
286
- ' version: "1.0.0",',
287
- ' ruleId: "R-1",',
288
- ' coversCondition: "risky write",',
289
- '};',
290
- 'export function evaluate(input, helpers) {',
291
- " if (helpers.isRiskPath() && helpers.getToolName() === 'write' && helpers.getPlanStatus() !== 'READY') {",
292
- " return { decision: 'requireApproval', matched: true, reason: 'plan required' };",
293
- ' }',
294
- " return { decision: 'allow', matched: false, reason: 'not-applicable' };",
295
- '}',
296
- ].join('\n'),
297
- });
298
- seedReplaySample('session-eval', 'artifact-eval');
299
-
300
- const result = handlePromoteImplCommand(makeCommandContext(workspaceDir, 'eval IMPL-CAND'));
301
-
302
- expect(result.text).toContain('Replay Evaluation Report');
303
- expect(result.text).toContain('Overall Decision: [PASS]');
304
- expect(
305
- fs.readdirSync(path.join(getImplementationAssetRoot(stateDir, 'IMPL-CAND'), 'replays')).length,
306
- ).toBeGreaterThan(0);
307
- });
308
-
309
- it('disable preserves the legacy training store while recording disable metadata', () => {
310
- seedLedger([makeImplementation('IMPL-ACTIVE', 'active')]);
311
-
312
- const result = handleDisableImplCommand(
313
- makeCommandContext(workspaceDir, 'IMPL-ACTIVE --reason "manual disable"')
314
- );
315
-
316
- expect(result.text).toContain('Implementation disabled');
317
- const updated = loadLedger(stateDir).tree.implementations['IMPL-ACTIVE'];
318
- expect(updated.lifecycleState).toBe('disabled');
319
- expect(updated.disabledReason).toBe('manual disable');
320
-
321
- const raw = JSON.parse(
322
- fs.readFileSync(path.join(stateDir, 'principle_training_state.json'), 'utf-8')
323
- ) as Record<string, unknown>;
324
- expect(raw['P-1']).toBeDefined();
325
- });
326
-
327
- it('archive preserves the legacy training store while archiving a candidate', () => {
328
- seedLedger([makeImplementation('IMPL-CAND', 'candidate')]);
329
-
330
- const result = handleArchiveImplCommand(makeCommandContext(workspaceDir, 'IMPL-CAND'));
331
-
332
- expect(result.text).toContain('Implementation archived');
333
- expect(loadLedger(stateDir).tree.implementations['IMPL-CAND'].lifecycleState).toBe('archived');
334
-
335
- const raw = JSON.parse(
336
- fs.readFileSync(path.join(stateDir, 'principle_training_state.json'), 'utf-8')
337
- ) as Record<string, unknown>;
338
- expect(raw['P-1']).toBeDefined();
339
- });
340
-
341
- it('rollback preserves the legacy training store while restoring the previous active implementation', () => {
342
- seedLedger([
343
- makeImplementation('IMPL-CURRENT', 'active', { previousActive: 'IMPL-PREV' }),
344
- makeImplementation('IMPL-PREV', 'disabled'),
345
- ]);
346
-
347
- const result = handleRollbackImplCommand(
348
- makeCommandContext(workspaceDir, 'IMPL-CURRENT --reason "rollback"')
349
- );
350
-
351
- expect(result.text).toContain('Rollback complete');
352
-
353
- const ledger = loadLedger(stateDir);
354
- expect(ledger.tree.implementations['IMPL-CURRENT'].lifecycleState).toBe('disabled');
355
- expect(ledger.tree.implementations['IMPL-PREV'].lifecycleState).toBe('active');
356
-
357
- const raw = JSON.parse(
358
- fs.readFileSync(path.join(stateDir, 'principle_training_state.json'), 'utf-8')
359
- ) as Record<string, unknown>;
360
- expect(raw['P-1']).toBeDefined();
361
- });
362
- });
@@ -1,63 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { DetectionFunnel } from '../../src/core/detection-funnel.js';
3
-
4
- describe('DetectionFunnel', () => {
5
- const mockDictionary = {
6
- match: vi.fn(),
7
- };
8
-
9
- beforeEach(() => {
10
- vi.clearAllMocks();
11
- });
12
-
13
-
14
- it('should skip protocol text before dictionary and queue checks', () => {
15
- const funnel = new DetectionFunnel(mockDictionary as any);
16
- const enqueueSpy = vi.spyOn(funnel as any, 'enqueueAsync');
17
-
18
- const result = funnel.detect('[EVOLUTION_ACK]');
19
-
20
- expect(result).toEqual({ detected: false, source: 'l1_exact' });
21
- expect(mockDictionary.match).not.toHaveBeenCalled();
22
- expect(enqueueSpy).not.toHaveBeenCalled();
23
- });
24
-
25
- it('L1: should return true immediately if dictionary matches', async () => {
26
- mockDictionary.match.mockReturnValue({ ruleId: 'P_CONFUSION', severity: 35 });
27
- const funnel = new DetectionFunnel(mockDictionary as any);
28
-
29
- const result = funnel.detect('I am confused');
30
-
31
- expect(result.detected).toBe(true);
32
- expect(result.source).toBe('l1_exact');
33
- expect(mockDictionary.match).toHaveBeenCalledWith('I am confused');
34
- });
35
-
36
- it('L2: should return true if found in cache after initial mismatch', async () => {
37
- mockDictionary.match.mockReturnValue(undefined);
38
- const funnel = new DetectionFunnel(mockDictionary as any);
39
-
40
- // Manually prime the cache for testing (or simulate a previous L3 hit)
41
- (funnel as any).cache.set('hash_123', { detected: true, severity: 40 });
42
-
43
- // We need a stable hash for the test
44
- vi.spyOn(funnel as any, 'computeHash').mockReturnValue('hash_123');
45
-
46
- const result = funnel.detect('Some repetitive text');
47
-
48
- expect(result.detected).toBe(true);
49
- expect(result.source).toBe('l2_cache');
50
- });
51
-
52
- it('L3: should enqueue for async detection if mismatch and not in cache', () => {
53
- mockDictionary.match.mockReturnValue(undefined);
54
- const funnel = new DetectionFunnel(mockDictionary as any);
55
- const enqueueSpy = vi.spyOn(funnel as any, 'enqueueAsync');
56
-
57
- const result = funnel.detect('New unknown expression');
58
-
59
- expect(result.detected).toBe(false);
60
- expect(result.source).toBe('l3_async_queued');
61
- expect(enqueueSpy).toHaveBeenCalledWith('New unknown expression');
62
- });
63
- });
@@ -1,58 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as os from 'os';
3
- import * as path from 'path';
4
- import { afterEach, describe, expect, it } from 'vitest';
5
- import { EvolutionReducerImpl } from '../../src/core/evolution-reducer.js';
6
- import { handleEvolutionStatusCommand } from '../../src/commands/evolution-status.js';
7
-
8
- const tempDirs: string[] = [];
9
- function makeTempDir(): string {
10
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-evolution-e2e-'));
11
- tempDirs.push(dir);
12
- return dir;
13
- }
14
-
15
- afterEach(() => {
16
- for (const dir of tempDirs.splice(0)) {
17
- fs.rmSync(dir, { recursive: true, force: true });
18
- }
19
- });
20
-
21
- describe('evolution loop e2e', () => {
22
- it('runs pain -> create principle -> status query -> rollback flow', () => {
23
- const workspace = makeTempDir();
24
- const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
25
-
26
- // Emit pain (no longer creates principle automatically)
27
- reducer.emitSync({
28
- ts: new Date().toISOString(),
29
- type: 'pain_detected',
30
- data: {
31
- painId: 'pain-e2e-1',
32
- painType: 'tool_failure',
33
- source: 'write',
34
- reason: 'write failed on risky file',
35
- },
36
- });
37
-
38
- // Create principle from diagnosis
39
- const principleId = reducer.createPrincipleFromDiagnosis({
40
- painId: 'pain-e2e-1',
41
- painType: 'tool_failure',
42
- triggerPattern: 'file write operation fails on risky files',
43
- action: 'verify file permissions before writing',
44
- source: 'write',
45
- });
46
-
47
- const p = reducer.getProbationPrinciples()[0];
48
- expect(p).toBeDefined();
49
-
50
- const statusBefore = handleEvolutionStatusCommand({ config: { workspaceDir: workspace, language: 'en' } } as any);
51
- expect(statusBefore.text).toContain('probation principles: 1');
52
-
53
- reducer.rollbackPrinciple(p.id, 'manual validation failed');
54
-
55
- const statusAfter = handleEvolutionStatusCommand({ config: { workspaceDir: workspace, language: 'en' } } as any);
56
- expect(statusAfter.text).toContain('deprecated principles: 1');
57
- });
58
- });