principles-disciple 1.87.0 → 1.89.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.
@@ -3,13 +3,13 @@ import { WorkspaceContext } from '../core/workspace-context.js';
3
3
  import { TrajectoryRegistry } from '../core/trajectory.js';
4
4
  import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
5
5
  import {
6
- WorkflowFunnelLoader,
7
6
  PiAiRuntimeAdapter,
8
7
  CorrectionObserver,
9
8
  AgentScheduler,
10
9
  } from '@principles/core/runtime-v2';
11
10
  import { KeywordOptimizationService } from './keyword-optimization-service.js';
12
11
  import { SystemLogger } from '../core/system-logger.js';
12
+ import { resolveObserverConfig } from '../core/pd-config-loader.js';
13
13
 
14
14
  export interface CorrectionObserverServiceShape {
15
15
  id: string;
@@ -26,43 +26,53 @@ const CORRECTION_OBSERVER_INITIAL_DELAY_MS = 10_000;
26
26
  const CORRECTION_OBSERVER_MAX_RECENT_SESSIONS = 20;
27
27
  const CORRECTION_OBSERVER_MAX_PAYLOAD_SESSIONS = 5;
28
28
 
29
+ /**
30
+ * PRI-307: Resolve CorrectionObserver from .pd/config.yaml.
31
+ *
32
+ * States:
33
+ * - disabled: feature flag off → return null, no noisy logs
34
+ * - needs_setup: enabled but missing API key or profile → return null with structured reason
35
+ * - ready/not_ready: enabled and configured → return observer instance
36
+ */
29
37
  export function resolveCorrectionObserver(wctx: WorkspaceContext, logger?: Pick<PluginLogger, 'info' | 'warn' | 'error' | 'debug'>): CorrectionObserver | null {
30
38
  try {
31
- const loader = new WorkflowFunnelLoader(wctx.stateDir);
32
- const funnel = loader.getFunnel('pd-correction-observer');
33
- const policy = funnel?.policy;
34
- if (!policy || policy.runtimeKind !== 'pi-ai') {
35
- logger?.debug?.('[PD:CorrectionObserver] workflows.yaml pd-correction-observer policy not found. Falling back to environment variables.');
36
- const provider = process.env.PD_CORRECTION_PROVIDER || 'anthropic';
37
- const model = process.env.PD_CORRECTION_MODEL || 'anthropic/claude-3-5-sonnet';
38
- const apiKeyEnv = process.env.PD_CORRECTION_API_KEY_ENV || 'ANTHROPIC_API_KEY';
39
- const baseUrl = process.env.PD_CORRECTION_BASE_URL;
40
-
41
- if (!process.env[apiKeyEnv]) {
42
- logger?.debug?.(`[PD:CorrectionObserver] API key env ${apiKeyEnv} is not set. Periodic optimization disabled.`);
43
- return null;
39
+ const observerConfig = resolveObserverConfig(
40
+ wctx.workspaceDir,
41
+ 'correction_observer',
42
+ 'correctionObserver',
43
+ logger,
44
+ );
45
+
46
+ if (!observerConfig.enabled) {
47
+ if (observerConfig.readiness === 'config_malformed') {
48
+ logger?.warn?.(`[PD:CorrectionObserver] Config malformed: ${observerConfig.reason}. ${observerConfig.nextAction}`);
49
+ } else {
50
+ logger?.debug?.(`[PD:CorrectionObserver] ${observerConfig.reason}`);
44
51
  }
52
+ return null;
53
+ }
54
+
55
+ if (observerConfig.readiness === 'needs_setup') {
56
+ logger?.info?.(`[PD:CorrectionObserver] ${observerConfig.reason}. ${observerConfig.nextAction}`);
57
+ return null;
58
+ }
45
59
 
60
+ // ready or not_ready — create the observer
61
+ if (observerConfig.runtimeProfileType === 'pi-ai') {
46
62
  const adapter = new PiAiRuntimeAdapter({
47
- provider,
48
- model,
49
- apiKeyEnv,
50
- baseUrl,
63
+ provider: observerConfig.provider ?? 'anthropic',
64
+ model: observerConfig.model ?? 'anthropic/claude-3-5-sonnet',
65
+ apiKeyEnv: observerConfig.apiKeyEnv ?? 'ANTHROPIC_API_KEY',
66
+ timeoutMs: observerConfig.timeoutMs ?? undefined,
67
+ baseUrl: observerConfig.baseUrl ?? undefined,
51
68
  workspace: wctx.workspaceDir,
52
69
  });
53
- return new CorrectionObserver({ runtimeAdapter: adapter });
70
+ return new CorrectionObserver({ runtimeAdapter: adapter }, { timeoutMs: observerConfig.timeoutMs ?? undefined });
54
71
  }
55
72
 
56
- const adapter = new PiAiRuntimeAdapter({
57
- provider: String(policy.provider),
58
- model: String(policy.model),
59
- apiKeyEnv: String(policy.apiKeyEnv),
60
- maxRetries: policy.maxRetries,
61
- timeoutMs: policy.timeoutMs ?? 30_000,
62
- baseUrl: policy.baseUrl,
63
- workspace: wctx.workspaceDir,
64
- });
65
- return new CorrectionObserver({ runtimeAdapter: adapter }, { timeoutMs: policy.timeoutMs });
73
+ // OpenClaw profile not yet supported for observer runtime
74
+ logger?.info?.(`[PD:CorrectionObserver] OpenClaw runtime profile not yet supported for correction observer. Skipping.`);
75
+ return null;
66
76
  } catch (err) {
67
77
  logger?.warn?.(`[PD:CorrectionObserver] Failed to resolve CorrectionObserver: ${String(err)}`);
68
78
  return null;
@@ -73,7 +83,8 @@ export async function runCorrectionObserverCycle(wctx: WorkspaceContext, logger:
73
83
  try {
74
84
  const observer = resolveCorrectionObserver(wctx, logger);
75
85
  if (!observer) {
76
- logger?.info?.('[PD:CorrectionObserver] Observer not resolved (no API key or config). Skipping cycle.');
86
+ // PRI-307: No noisy "no API key" cycling. Only log at debug level.
87
+ logger?.debug?.(`[PD:CorrectionObserver] Observer not resolved. Skipping cycle.`);
77
88
  return;
78
89
  }
79
90
 
@@ -163,8 +174,28 @@ export const CorrectionObserverService: CorrectionObserverServiceShape = {
163
174
  if (logger) logger.info(`[PD:CorrectionObserver] Already started for workspace: ${workspaceDir}. Skipping duplicate start.`);
164
175
  return;
165
176
  }
166
- startedWorkspaces.add(workspaceDir);
167
177
 
178
+ // PRI-307: Check observer config before starting
179
+ const observerConfig = resolveObserverConfig(
180
+ workspaceDir,
181
+ 'correction_observer',
182
+ 'correctionObserver',
183
+ logger,
184
+ );
185
+
186
+ if (!observerConfig.enabled) {
187
+ // Disabled → no start, no noisy cycling. Single structured log.
188
+ logger?.info?.(`[PD:CorrectionObserver] ${observerConfig.reason}. ${observerConfig.nextAction}`);
189
+ return;
190
+ }
191
+
192
+ if (observerConfig.readiness === 'needs_setup') {
193
+ // Enabled but missing setup → structured needs_setup, no noisy cycling
194
+ logger?.info?.(`[PD:CorrectionObserver] ${observerConfig.reason}. ${observerConfig.nextAction}`);
195
+ return;
196
+ }
197
+
198
+ startedWorkspaces.add(workspaceDir);
168
199
  correctionObserverStopped = false;
169
200
 
170
201
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
@@ -0,0 +1,407 @@
1
+ /**
2
+ * pd-config-loader (plugin) tests — PRI-307
3
+ *
4
+ * Covers:
5
+ * - Observer disabled → no start, no noisy logs
6
+ * - Observer enabled + missing setup → needs_setup + nextAction
7
+ * - Observer enabled + configured → ready
8
+ * - No secret output in any result
9
+ * - Feature flag loading from .pd/config.yaml
10
+ * - Missing config → defaults
11
+ * - Malformed config → fail loud
12
+ */
13
+
14
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import * as os from 'node:os';
18
+ import * as yaml from 'js-yaml';
19
+ import {
20
+ loadPdConfigForPlugin,
21
+ loadFeatureFlagFromConfig,
22
+ resolveObserverConfig,
23
+ getPdConfigPath,
24
+ type ObserverConfigResult,
25
+ } from '../../src/core/pd-config-loader.js';
26
+
27
+ // ── Helpers ─────────────────────────────────────────────────────────────────
28
+
29
+ function mkTmpDir(): string {
30
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'pd-plugin-config-test-'));
31
+ }
32
+
33
+ function rmTmpDir(dir: string): void {
34
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
35
+ }
36
+
37
+ function writeConfig(workspaceDir: string, content: string): void {
38
+ const configDir = path.join(workspaceDir, '.pd');
39
+ fs.mkdirSync(configDir, { recursive: true });
40
+ fs.writeFileSync(path.join(configDir, 'config.yaml'), content, 'utf8');
41
+ }
42
+
43
+ function makeValidConfigWithObserverEnabled(): string {
44
+ return yaml.dump({
45
+ version: 1,
46
+ features: {
47
+ prompt: { category: 'core', enabled: true },
48
+ code_tool_hook: { category: 'core', enabled: true },
49
+ defer_archive: { category: 'core', enabled: true },
50
+ correction_observer: { category: 'quiet', enabled: true },
51
+ empathy_observer: { category: 'quiet', enabled: false },
52
+ },
53
+ runtimeProfiles: {
54
+ 'openclaw.default': { type: 'openclaw', source: 'default' },
55
+ 'pd.anthropic-sonnet': { type: 'pi-ai', provider: 'anthropic', model: 'claude-3-5-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY', timeoutMs: 300000 },
56
+ },
57
+ internalAgents: {
58
+ defaultRuntime: 'openclaw.default',
59
+ agents: {
60
+ diagnostician: { enabled: true },
61
+ dreamer: { enabled: true },
62
+ scribe: { enabled: true },
63
+ artificer: { enabled: true },
64
+ philosopher: { enabled: false },
65
+ evaluator: { enabled: false },
66
+ rolloutReviewer: { enabled: false },
67
+ trainer: { enabled: false },
68
+ correctionObserver: { enabled: true, runtimeProfile: 'pd.anthropic-sonnet' },
69
+ empathyObserver: { enabled: false },
70
+ },
71
+ },
72
+ ui: { diagnostics: { mode: 'simple' } },
73
+ });
74
+ }
75
+
76
+ function makeValidConfigWithObserverDisabled(): string {
77
+ return yaml.dump({
78
+ version: 1,
79
+ features: {
80
+ prompt: { category: 'core', enabled: true },
81
+ code_tool_hook: { category: 'core', enabled: true },
82
+ defer_archive: { category: 'core', enabled: true },
83
+ correction_observer: { category: 'quiet', enabled: false },
84
+ empathy_observer: { category: 'quiet', enabled: false },
85
+ },
86
+ runtimeProfiles: {
87
+ 'openclaw.default': { type: 'openclaw', source: 'default' },
88
+ },
89
+ internalAgents: {
90
+ defaultRuntime: 'openclaw.default',
91
+ agents: {
92
+ diagnostician: { enabled: true },
93
+ dreamer: { enabled: true },
94
+ scribe: { enabled: true },
95
+ artificer: { enabled: true },
96
+ philosopher: { enabled: false },
97
+ evaluator: { enabled: false },
98
+ rolloutReviewer: { enabled: false },
99
+ trainer: { enabled: false },
100
+ correctionObserver: { enabled: false },
101
+ empathyObserver: { enabled: false },
102
+ },
103
+ },
104
+ ui: { diagnostics: { mode: 'simple' } },
105
+ });
106
+ }
107
+
108
+ // ── Observer disabled ────────────────────────────────────────────────────────
109
+
110
+ describe('Observer disabled', () => {
111
+ it('returns readiness=disabled when feature flag is off', () => {
112
+ const tmp = mkTmpDir();
113
+ writeConfig(tmp, makeValidConfigWithObserverDisabled());
114
+ try {
115
+ const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
116
+ expect(result.enabled).toBe(false);
117
+ expect(result.readiness).toBe('disabled');
118
+ expect(result.reason).toContain('disabled');
119
+ expect(result.nextAction).toContain('.pd/config.yaml');
120
+ } finally { rmTmpDir(tmp); }
121
+ });
122
+
123
+ it('returns readiness=disabled when config is missing (defaults)', () => {
124
+ const tmp = mkTmpDir();
125
+ try {
126
+ const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
127
+ expect(result.enabled).toBe(false);
128
+ expect(result.readiness).toBe('disabled');
129
+ } finally { rmTmpDir(tmp); }
130
+ });
131
+ });
132
+
133
+ // ── Observer enabled + missing setup → needs_setup ──────────────────────────
134
+
135
+ describe('Observer needs_setup', () => {
136
+ it('returns readiness=needs_setup when API key env is not set', () => {
137
+ const tmp = mkTmpDir();
138
+ writeConfig(tmp, makeValidConfigWithObserverEnabled());
139
+ const originalKey = process.env.ANTHROPIC_API_KEY;
140
+ delete process.env.ANTHROPIC_API_KEY;
141
+ try {
142
+ const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
143
+ expect(result.enabled).toBe(true);
144
+ expect(result.readiness).toBe('needs_setup');
145
+ expect(result.apiKeyEnv).toBe('ANTHROPIC_API_KEY');
146
+ expect(result.apiKeyPresent).toBe(false);
147
+ expect(result.nextAction).toContain('ANTHROPIC_API_KEY');
148
+ } finally {
149
+ if (originalKey !== undefined) process.env.ANTHROPIC_API_KEY = originalKey;
150
+ rmTmpDir(tmp);
151
+ }
152
+ });
153
+
154
+ it('returns readiness=needs_setup when runtime profile is not found', () => {
155
+ const tmp = mkTmpDir();
156
+ const config = yaml.dump({
157
+ version: 1,
158
+ features: {
159
+ prompt: { category: 'core', enabled: true },
160
+ code_tool_hook: { category: 'core', enabled: true },
161
+ defer_archive: { category: 'core', enabled: true },
162
+ correction_observer: { category: 'quiet', enabled: true },
163
+ empathy_observer: { category: 'quiet', enabled: false },
164
+ },
165
+ runtimeProfiles: {
166
+ 'openclaw.default': { type: 'openclaw', source: 'default' },
167
+ },
168
+ internalAgents: {
169
+ defaultRuntime: 'openclaw.default',
170
+ agents: {
171
+ diagnostician: { enabled: true },
172
+ dreamer: { enabled: true },
173
+ scribe: { enabled: true },
174
+ artificer: { enabled: true },
175
+ philosopher: { enabled: false },
176
+ evaluator: { enabled: false },
177
+ rolloutReviewer: { enabled: false },
178
+ trainer: { enabled: false },
179
+ correctionObserver: { enabled: true, runtimeProfile: 'nonexistent.profile' },
180
+ empathyObserver: { enabled: false },
181
+ },
182
+ },
183
+ });
184
+ writeConfig(tmp, config);
185
+ try {
186
+ const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
187
+ expect(result.enabled).toBe(true);
188
+ expect(result.readiness).toBe('needs_setup');
189
+ expect(result.reason).toContain('not found');
190
+ } finally { rmTmpDir(tmp); }
191
+ });
192
+ });
193
+
194
+ // ── Observer ready ───────────────────────────────────────────────────────────
195
+
196
+ describe('Observer ready', () => {
197
+ it('returns readiness=not_ready when pi-ai profile has API key set', () => {
198
+ const tmp = mkTmpDir();
199
+ writeConfig(tmp, makeValidConfigWithObserverEnabled());
200
+ const originalKey = process.env.ANTHROPIC_API_KEY;
201
+ process.env.ANTHROPIC_API_KEY = 'sk-ant-test-key-1234567890';
202
+ try {
203
+ const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
204
+ expect(result.enabled).toBe(true);
205
+ expect(result.readiness).toBe('not_ready');
206
+ expect(result.apiKeyPresent).toBe(true);
207
+ expect(result.provider).toBe('anthropic');
208
+ expect(result.model).toBe('claude-3-5-sonnet');
209
+ } finally {
210
+ if (originalKey !== undefined) process.env.ANTHROPIC_API_KEY = originalKey;
211
+ else delete process.env.ANTHROPIC_API_KEY;
212
+ rmTmpDir(tmp);
213
+ }
214
+ });
215
+
216
+ it('returns readiness=needs_setup for OpenClaw profile (not supported for observers)', () => {
217
+ const tmp = mkTmpDir();
218
+ const config = yaml.dump({
219
+ version: 1,
220
+ features: {
221
+ prompt: { category: 'core', enabled: true },
222
+ code_tool_hook: { category: 'core', enabled: true },
223
+ defer_archive: { category: 'core', enabled: true },
224
+ correction_observer: { category: 'quiet', enabled: true },
225
+ empathy_observer: { category: 'quiet', enabled: false },
226
+ },
227
+ runtimeProfiles: {
228
+ 'openclaw.default': { type: 'openclaw', source: 'default' },
229
+ },
230
+ internalAgents: {
231
+ defaultRuntime: 'openclaw.default',
232
+ agents: {
233
+ diagnostician: { enabled: true },
234
+ dreamer: { enabled: true },
235
+ scribe: { enabled: true },
236
+ artificer: { enabled: true },
237
+ philosopher: { enabled: false },
238
+ evaluator: { enabled: false },
239
+ rolloutReviewer: { enabled: false },
240
+ trainer: { enabled: false },
241
+ correctionObserver: { enabled: true },
242
+ empathyObserver: { enabled: false },
243
+ },
244
+ },
245
+ });
246
+ writeConfig(tmp, config);
247
+ try {
248
+ const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
249
+ expect(result.enabled).toBe(true);
250
+ expect(result.readiness).toBe('needs_setup');
251
+ expect(result.runtimeProfileType).toBe('openclaw');
252
+ expect(result.reason).toContain('not supported');
253
+ expect(result.nextAction).toContain('pi-ai');
254
+ } finally { rmTmpDir(tmp); }
255
+ });
256
+ });
257
+
258
+ // ── No secret output ─────────────────────────────────────────────────────────
259
+
260
+ describe('No secret output', () => {
261
+ it('observer config result never contains API key values', () => {
262
+ const tmp = mkTmpDir();
263
+ writeConfig(tmp, makeValidConfigWithObserverEnabled());
264
+ const originalKey = process.env.ANTHROPIC_API_KEY;
265
+ process.env.ANTHROPIC_API_KEY = 'sk-ant-test-key-1234567890';
266
+ try {
267
+ const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
268
+ const json = JSON.stringify(result);
269
+ expect(json).not.toContain('sk-ant-test-key');
270
+ expect(json).not.toContain('sk-ant-');
271
+ expect(json).not.toMatch(/"apiKey"\s*:/);
272
+ } finally {
273
+ if (originalKey !== undefined) process.env.ANTHROPIC_API_KEY = originalKey;
274
+ else delete process.env.ANTHROPIC_API_KEY;
275
+ rmTmpDir(tmp);
276
+ }
277
+ });
278
+ });
279
+
280
+ // ── Feature flag loading ─────────────────────────────────────────────────────
281
+
282
+ describe('Feature flag loading from .pd/config.yaml', () => {
283
+ it('loadFeatureFlagFromConfig returns enabled for MVP core flags', () => {
284
+ const tmp = mkTmpDir();
285
+ try {
286
+ const result = loadFeatureFlagFromConfig(tmp, 'prompt');
287
+ expect(result.enabled).toBe(true);
288
+ expect(result.source).toBe('defaults');
289
+ } finally { rmTmpDir(tmp); }
290
+ });
291
+
292
+ it('loadFeatureFlagFromConfig returns disabled for quiet flags by default', () => {
293
+ const tmp = mkTmpDir();
294
+ try {
295
+ const result = loadFeatureFlagFromConfig(tmp, 'correction_observer');
296
+ expect(result.enabled).toBe(false);
297
+ } finally { rmTmpDir(tmp); }
298
+ });
299
+
300
+ it('loadFeatureFlagFromConfig reads from user config', () => {
301
+ const tmp = mkTmpDir();
302
+ writeConfig(tmp, makeValidConfigWithObserverEnabled());
303
+ try {
304
+ const result = loadFeatureFlagFromConfig(tmp, 'correction_observer');
305
+ expect(result.enabled).toBe(true);
306
+ expect(result.source).toBe('user_config');
307
+ } finally { rmTmpDir(tmp); }
308
+ });
309
+ });
310
+
311
+ // ── Plugin config load ───────────────────────────────────────────────────────
312
+
313
+ describe('Plugin config load', () => {
314
+ it('returns ok=true with defaults when config is missing', () => {
315
+ const tmp = mkTmpDir();
316
+ try {
317
+ const result = loadPdConfigForPlugin(tmp);
318
+ expect(result.ok).toBe(true);
319
+ expect(result.source).toBe('defaults');
320
+ expect(result.effective.config.version).toBe(1);
321
+ } finally { rmTmpDir(tmp); }
322
+ });
323
+
324
+ it('returns ok=false with errors for malformed config', () => {
325
+ const tmp = mkTmpDir();
326
+ writeConfig(tmp, 'version: [unterminated');
327
+ try {
328
+ const result = loadPdConfigForPlugin(tmp);
329
+ expect(result.ok).toBe(false);
330
+ expect(result.source).toBe('malformed');
331
+ expect(result.errors.length).toBeGreaterThan(0);
332
+ expect(result.effective.config.version).toBe(1); // defaults still available
333
+ } finally { rmTmpDir(tmp); }
334
+ });
335
+ });
336
+
337
+ // ── Config malformed → fail loud ────────────────────────────────────────────
338
+
339
+ describe('Config malformed → fail loud', () => {
340
+ it('returns readiness=config_malformed when config is invalid YAML', () => {
341
+ const tmp = mkTmpDir();
342
+ writeConfig(tmp, 'version: [unterminated');
343
+ try {
344
+ const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
345
+ expect(result.enabled).toBe(false);
346
+ expect(result.readiness).toBe('config_malformed');
347
+ expect(result.reason).toContain('Config validation failed');
348
+ expect(result.nextAction).toBeTruthy();
349
+ expect(result.configErrors).toBeDefined();
350
+ expect(result.configErrors!.length).toBeGreaterThan(0);
351
+ } finally { rmTmpDir(tmp); }
352
+ });
353
+
354
+ it('returns readiness=config_malformed for invalid version', () => {
355
+ const tmp = mkTmpDir();
356
+ writeConfig(tmp, yaml.dump({ version: 99, features: {}, runtimeProfiles: {}, internalAgents: { defaultRuntime: 'x', agents: {} } }));
357
+ try {
358
+ const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
359
+ expect(result.readiness).toBe('config_malformed');
360
+ } finally { rmTmpDir(tmp); }
361
+ });
362
+ });
363
+
364
+ // ── Feature flag vs agent enabled mismatch ──────────────────────────────────
365
+
366
+ describe('Feature flag vs agent enabled mismatch', () => {
367
+ it('returns readiness=disabled when feature flag is on but agent.enabled=false', () => {
368
+ const tmp = mkTmpDir();
369
+ const config = yaml.dump({
370
+ version: 1,
371
+ features: {
372
+ prompt: { category: 'core', enabled: true },
373
+ code_tool_hook: { category: 'core', enabled: true },
374
+ defer_archive: { category: 'core', enabled: true },
375
+ correction_observer: { category: 'quiet', enabled: true }, // feature flag ON
376
+ empathy_observer: { category: 'quiet', enabled: false },
377
+ },
378
+ runtimeProfiles: {
379
+ 'openclaw.default': { type: 'openclaw', source: 'default' },
380
+ 'pd.anthropic-sonnet': { type: 'pi-ai', provider: 'anthropic', model: 'claude-3-5-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY' },
381
+ },
382
+ internalAgents: {
383
+ defaultRuntime: 'openclaw.default',
384
+ agents: {
385
+ diagnostician: { enabled: true },
386
+ dreamer: { enabled: true },
387
+ scribe: { enabled: true },
388
+ artificer: { enabled: true },
389
+ philosopher: { enabled: false },
390
+ evaluator: { enabled: false },
391
+ rolloutReviewer: { enabled: false },
392
+ trainer: { enabled: false },
393
+ correctionObserver: { enabled: false }, // agent.enabled OFF
394
+ empathyObserver: { enabled: false },
395
+ },
396
+ },
397
+ });
398
+ writeConfig(tmp, config);
399
+ try {
400
+ const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
401
+ expect(result.enabled).toBe(false);
402
+ expect(result.readiness).toBe('disabled');
403
+ expect(result.reason).toContain('enabled is false');
404
+ expect(result.nextAction).toContain('internalAgents.agents.correctionObserver.enabled=true');
405
+ } finally { rmTmpDir(tmp); }
406
+ });
407
+ });
@@ -324,7 +324,7 @@ describe('surface-guard', () => {
324
324
 
325
325
  it('no quiet surface disabledReason promises a feature flag override (PRI-298 / chatgpt P2)', () => {
326
326
  // The runtime guard path (`isSurfaceEnabled(surfaceId)` with no
327
- // overrides argument) does not consume `.pd/feature-flags.yaml`, so
327
+ // overrides argument) does not consume `.pd/config.yaml`, so
328
328
  // telling operators to "enable via feature flag override" would be
329
329
  // an impossible next action. Quiet copy must describe the surface
330
330
  // honestly without pointing to a non-existent override path.
@@ -124,6 +124,7 @@ describe('PRI-212 plugin core anti-growth guard', () => {
124
124
  'runtime-v2-prompt-activation-reader.ts',
125
125
  'workspace-guidance-migrator.ts',
126
126
  'surface-guard.ts',
127
+ 'pd-config-loader.ts',
127
128
  ] as const;
128
129
 
129
130
  // Category 6: Test files