principles-disciple 1.85.0 → 1.86.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.85.0",
5
+ "version": "1.86.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.85.0",
3
+ "version": "1.86.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -65,7 +65,7 @@ vi.mock('@principles/core/runtime-v2', () => {
65
65
  };
66
66
  });
67
67
 
68
- import { CorrectionObserverService, runCorrectionObserverCycle } from '../../src/service/correction-observer-service.js';
68
+ import { CorrectionObserverService, runCorrectionObserverCycle, resolveCorrectionObserver } from '../../src/service/correction-observer-service.js';
69
69
  import { safeRmDir } from '../test-utils.js';
70
70
 
71
71
  describe('CorrectionObserverService — Independent Service (PRI-293)', () => {
@@ -329,3 +329,51 @@ describe('runCorrectionObserverCycle — Independent Execution', () => {
329
329
  }
330
330
  });
331
331
  });
332
+
333
+ describe('resolveCorrectionObserver — Configuration Resolution', () => {
334
+ beforeEach(() => {
335
+ vi.clearAllMocks();
336
+ });
337
+
338
+ it('returns observer when API key env is set with mocked policy', async () => {
339
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-resolve-'));
340
+ const stateDir = path.join(workspaceDir, '.state');
341
+ fs.mkdirSync(stateDir, { recursive: true });
342
+
343
+ process.env.ANTHROPIC_API_KEY = 'test-key';
344
+
345
+ const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
346
+
347
+ try {
348
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
349
+ const result = resolveCorrectionObserver(wctx, logger as any);
350
+
351
+ // With mocked WorkflowFunnelLoader returning valid policy, should return observer
352
+ expect(result).not.toBeNull();
353
+ } finally {
354
+ delete process.env.ANTHROPIC_API_KEY;
355
+ safeRmDir(workspaceDir);
356
+ }
357
+ });
358
+
359
+ it('returns observer when workflows.yaml provides valid policy', async () => {
360
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-policy-'));
361
+ const stateDir = path.join(workspaceDir, '.state');
362
+ fs.mkdirSync(stateDir, { recursive: true });
363
+
364
+ process.env.ANTHROPIC_API_KEY = 'test-key';
365
+
366
+ const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
367
+
368
+ try {
369
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
370
+ const result = resolveCorrectionObserver(wctx, logger as any);
371
+
372
+ // With mocked WorkflowFunnelLoader returning valid policy, should return observer
373
+ expect(result).not.toBeNull();
374
+ } finally {
375
+ delete process.env.ANTHROPIC_API_KEY;
376
+ safeRmDir(workspaceDir);
377
+ }
378
+ });
379
+ });
@@ -7,6 +7,10 @@ import {
7
7
  resolveCanonicalWorkspaceDir,
8
8
  resolveHookWorkspaceDir,
9
9
  resolveToolHookWorkspaceDirSafe,
10
+ resolveCommandWorkspaceDir,
11
+ resolvePluginCommandWorkspaceDir,
12
+ resolveWorkspaceDirForRuntimeV2,
13
+ WorkspaceResolutionError,
10
14
  } from '../../src/utils/workspace-resolver.js';
11
15
  import type { CanonicalWorkspaceResult, HookWorkspaceResolutionResult } from '../../src/utils/workspace-resolver.js';
12
16
 
@@ -345,3 +349,176 @@ describe('resolveToolHookWorkspaceDirSafe (backward compat)', () => {
345
349
  expect(fullWarn).toContain('principles-disciple.json');
346
350
  });
347
351
  });
352
+
353
+ describe('resolveCommandWorkspaceDir — Command Resolution', () => {
354
+ const originalEnv = { ...process.env };
355
+ const logger = {
356
+ error: vi.fn(),
357
+ warn: vi.fn(),
358
+ info: vi.fn(),
359
+ debug: vi.fn(),
360
+ };
361
+
362
+ const api = {
363
+ runtime: {
364
+ agent: {
365
+ resolveAgentWorkspaceDir: vi.fn(),
366
+ },
367
+ },
368
+ config: {},
369
+ logger,
370
+ };
371
+
372
+ beforeEach(() => {
373
+ process.env = { ...originalEnv };
374
+ delete process.env.PD_WORKSPACE_DIR;
375
+ delete process.env.OPENCLAW_WORKSPACE;
376
+ vi.clearAllMocks();
377
+ ensureDir(validWorkspace);
378
+ });
379
+
380
+ afterEach(() => {
381
+ process.env = { ...originalEnv };
382
+ });
383
+
384
+ it('returns ctx.workspaceDir when valid', () => {
385
+ const result = resolveCommandWorkspaceDir(api as any, { workspaceDir: validWorkspace });
386
+ expect(result).toBe(validWorkspace);
387
+ });
388
+
389
+ it('throws when ctx.workspaceDir is home directory', () => {
390
+ expect(() => resolveCommandWorkspaceDir(api as any, { workspaceDir: homeDir }))
391
+ .toThrow(/is invalid/);
392
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('is invalid'));
393
+ });
394
+
395
+ it('throws when ctx.workspaceDir is empty string', () => {
396
+ expect(() => resolveCommandWorkspaceDir(api as any, { workspaceDir: '' }))
397
+ .toThrow(/Cannot resolve workspace directory/);
398
+ });
399
+
400
+ it('falls back to API resolution when ctx.workspaceDir is undefined', () => {
401
+ api.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(validWorkspace);
402
+ process.env.OPENCLAW_WORKSPACE = validWorkspace;
403
+ const result = resolveCommandWorkspaceDir(api as any, {});
404
+ expect(result).toBe(path.resolve(validWorkspace));
405
+ });
406
+
407
+ it('falls back to PathResolver default when API throws', () => {
408
+ api.runtime.agent.resolveAgentWorkspaceDir.mockImplementation(() => {
409
+ throw new Error('API unavailable');
410
+ });
411
+ // PathResolver provides default ~/.openclaw/workspace fallback
412
+ const result = resolveCommandWorkspaceDir(api as any, {});
413
+ expect(result).toBeDefined();
414
+ expect(result).toContain('.openclaw');
415
+ });
416
+ });
417
+
418
+ describe('resolvePluginCommandWorkspaceDir — Plugin Command Resolution', () => {
419
+ beforeEach(() => {
420
+ vi.clearAllMocks();
421
+ ensureDir(validWorkspace);
422
+ });
423
+
424
+ it('returns ctx.workspaceDir when valid', () => {
425
+ const ctx = { workspaceDir: validWorkspace, config: {} };
426
+ const result = resolvePluginCommandWorkspaceDir(ctx as any, 'test-source');
427
+ expect(result).toBe(validWorkspace);
428
+ });
429
+
430
+ it('throws when ctx.workspaceDir is home directory', () => {
431
+ const ctx = { workspaceDir: homeDir, config: {} };
432
+ expect(() => resolvePluginCommandWorkspaceDir(ctx as any, 'test-source'))
433
+ .toThrow(/is invalid/);
434
+ });
435
+
436
+ it('falls back to ctx.config.workspaceDir when ctx.workspaceDir is undefined', () => {
437
+ const ctx = { workspaceDir: undefined, config: { workspaceDir: validWorkspace } };
438
+ const result = resolvePluginCommandWorkspaceDir(ctx as any, 'test-source');
439
+ expect(result).toBe(validWorkspace);
440
+ });
441
+
442
+ it('throws when both ctx.workspaceDir and ctx.config.workspaceDir are invalid', () => {
443
+ const ctx = { workspaceDir: homeDir, config: { workspaceDir: homeDir } };
444
+ expect(() => resolvePluginCommandWorkspaceDir(ctx as any, 'test-source'))
445
+ .toThrow(/is invalid/);
446
+ });
447
+
448
+ it('throws critical error when no workspaceDir available', () => {
449
+ const ctx = { workspaceDir: undefined, config: {} };
450
+ expect(() => resolvePluginCommandWorkspaceDir(ctx as any, 'test-source'))
451
+ .toThrow(/CRITICAL: workspaceDir is not set/);
452
+ });
453
+ });
454
+
455
+ describe('resolveWorkspaceDirForRuntimeV2 — Runtime V2 Resolution', () => {
456
+ beforeEach(() => {
457
+ vi.clearAllMocks();
458
+ ensureDir(validWorkspace);
459
+ });
460
+
461
+ it('returns normalized workspaceDir when valid', () => {
462
+ const result = resolveWorkspaceDirForRuntimeV2(
463
+ { workspaceDir: validWorkspace },
464
+ undefined,
465
+ 'runtime-v2-test',
466
+ );
467
+ expect(result).toBe(path.resolve(validWorkspace));
468
+ });
469
+
470
+ it('throws WorkspaceResolutionError when workspaceDir is empty', () => {
471
+ expect(() => resolveWorkspaceDirForRuntimeV2({ workspaceDir: '' }, undefined, 'test'))
472
+ .toThrow(WorkspaceResolutionError);
473
+
474
+ try {
475
+ resolveWorkspaceDirForRuntimeV2({ workspaceDir: '' }, undefined, 'test');
476
+ } catch (e) {
477
+ expect((e as WorkspaceResolutionError).reason).toBe('workspace_dir_missing');
478
+ expect((e as WorkspaceResolutionError).nextAction).toContain('PD_WORKSPACE_DIR');
479
+ }
480
+ });
481
+
482
+ it('throws WorkspaceResolutionError when workspaceDir is undefined', () => {
483
+ expect(() => resolveWorkspaceDirForRuntimeV2({}, undefined, 'test'))
484
+ .toThrow(WorkspaceResolutionError);
485
+ });
486
+
487
+ it('throws WorkspaceResolutionError when workspaceDir is home directory', () => {
488
+ expect(() => resolveWorkspaceDirForRuntimeV2({ workspaceDir: homeDir }, undefined, 'test'))
489
+ .toThrow(WorkspaceResolutionError);
490
+
491
+ try {
492
+ resolveWorkspaceDirForRuntimeV2({ workspaceDir: homeDir }, undefined, 'test');
493
+ } catch (e) {
494
+ expect((e as WorkspaceResolutionError).reason).toBe('workspace_dir_invalid');
495
+ }
496
+ });
497
+ });
498
+
499
+ describe('WorkspaceResolutionError — Error Structure', () => {
500
+ it('has correct name and properties', () => {
501
+ const error = new WorkspaceResolutionError(
502
+ 'Test message',
503
+ 'test_reason',
504
+ 'Test next action',
505
+ );
506
+ expect(error.name).toBe('WorkspaceResolutionError');
507
+ expect(error.message).toBe('Test message');
508
+ expect(error.reason).toBe('test_reason');
509
+ expect(error.nextAction).toBe('Test next action');
510
+ });
511
+
512
+ it('toJSON returns structured failure object', () => {
513
+ const error = new WorkspaceResolutionError(
514
+ 'Test message',
515
+ 'test_reason',
516
+ 'Test next action',
517
+ );
518
+ const json = error.toJSON();
519
+ expect(json.ok).toBe(false);
520
+ expect(json.reason).toBe('test_reason');
521
+ expect(json.message).toBe('Test message');
522
+ expect(json.nextAction).toBe('Test next action');
523
+ });
524
+ });