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.
@@ -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.87.0",
5
+ "version": "1.89.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.87.0",
3
+ "version": "1.89.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -7,6 +7,7 @@ import type { EmpathyEventStats } from '../types/event-types.js';
7
7
  import type { EvolutionLoopEvent } from '../core/evolution-types.js';
8
8
  import { computeHash } from '../utils/hashing.js';
9
9
  import { PainToPrincipleService, PrincipleTreeLedgerAdapter } from '@principles/core/runtime-v2';
10
+ import { loadPdConfigForPlugin } from '../core/pd-config-loader.js';
10
11
 
11
12
  /**
12
13
  * Creates a visual progress bar (e.g., [██████░░░░])
@@ -310,12 +311,16 @@ export async function handlePainReportCommand(ctx: PluginCommandContext): Promis
310
311
 
311
312
  try {
312
313
  const ledgerAdapter = new PrincipleTreeLedgerAdapter({ stateDir: wctx.stateDir });
314
+ // PRI-306: Load .pd/config.yaml for config-driven runtime binding
315
+ const configResult = loadPdConfigForPlugin(wctx.workspaceDir);
313
316
  const service = new PainToPrincipleService({
314
317
  workspaceDir: wctx.workspaceDir,
315
318
  stateDir: wctx.stateDir,
316
319
  ledgerAdapter,
317
320
  owner: 'openclaw-plugin',
318
321
  autoIntakeEnabled: true,
322
+ effectiveConfig: configResult.effective,
323
+ getEnvVar: (name: string) => process.env[name],
319
324
  });
320
325
 
321
326
  const result = await service.recordPain({
@@ -0,0 +1,400 @@
1
+ /**
2
+ * PD Config Loader (Plugin I/O boundary) — PRI-307
3
+ *
4
+ * Reads `.pd/config.yaml`, validates via core, computes effective config.
5
+ * Replaces the old `.pd/feature-flags.yaml` and `.state/workflows.yaml` reading
6
+ * for plugin production paths.
7
+ *
8
+ * ADR-0016: PD owns exactly one user config file.
9
+ * - Missing config → defaults with nextAction
10
+ * - Malformed config → fail loud with errors and nextAction
11
+ * - No secrets in output
12
+ * - Observer disabled → no start / no noisy log cycling
13
+ * - Observer enabled + missing setup → structured needs_setup + nextAction
14
+ */
15
+
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+ import yaml from 'js-yaml';
19
+ import {
20
+ validatePdConfig,
21
+ computeEffectivePdConfig,
22
+ computeFeatureFlagsFromConfig,
23
+ INTERNAL_AGENT_NAMES,
24
+ } from '@principles/core/runtime-v2';
25
+ import type {
26
+ EffectivePdConfig,
27
+ PdConfigValidationResult,
28
+ InternalAgentName,
29
+ } from '@principles/core/runtime-v2';
30
+
31
+ // ── Constants ────────────────────────────────────────────────────────────────
32
+
33
+ export const PD_CONFIG_DIR = '.pd';
34
+ export const PD_CONFIG_FILENAME = 'config.yaml';
35
+
36
+ // ── Types ────────────────────────────────────────────────────────────────────
37
+
38
+ export type ObserverReadiness = 'disabled' | 'needs_setup' | 'ready' | 'not_ready' | 'config_malformed';
39
+
40
+ export interface ObserverConfigResult {
41
+ /** Whether the observer feature is enabled in config */
42
+ enabled: boolean;
43
+ /** Observer readiness state */
44
+ readiness: ObserverReadiness;
45
+ /** Config source: 'defaults' | 'user_config' | 'malformed' */
46
+ source: string;
47
+ /** Reason for current state */
48
+ reason: string;
49
+ /** What the user should do next */
50
+ nextAction: string;
51
+ /** The runtime profile ID for this observer, if configured */
52
+ runtimeProfileId: string | null;
53
+ /** The runtime profile type, if configured */
54
+ runtimeProfileType: string | null;
55
+ /** The apiKeyEnv for the runtime profile, if applicable */
56
+ apiKeyEnv: string | null;
57
+ /** Whether the apiKeyEnv is present in process.env */
58
+ apiKeyPresent: boolean;
59
+ /** Provider name from runtime profile */
60
+ provider: string | null;
61
+ /** Model name from runtime profile */
62
+ model: string | null;
63
+ /** Timeout from runtime profile */
64
+ timeoutMs: number | null;
65
+ /** Base URL from runtime profile */
66
+ baseUrl: string | null;
67
+ /** Config validation errors (only present when readiness=config_malformed) */
68
+ configErrors?: Array<{ path: string; reason: string; nextAction: string }>;
69
+ }
70
+
71
+ export interface PluginConfigLoadResult {
72
+ ok: boolean;
73
+ effective: EffectivePdConfig;
74
+ source: 'defaults' | 'user_config' | 'malformed';
75
+ configPath: string;
76
+ warnings: string[];
77
+ errors: Array<{ path: string; reason: string; nextAction: string }>;
78
+ }
79
+
80
+ // ── Config Path ──────────────────────────────────────────────────────────────
81
+
82
+ export function getPdConfigPath(workspaceDir: string): string {
83
+ return path.join(workspaceDir, PD_CONFIG_DIR, PD_CONFIG_FILENAME);
84
+ }
85
+
86
+ // ── Load PD Config ───────────────────────────────────────────────────────────
87
+
88
+ /**
89
+ * Load and validate `.pd/config.yaml` from the workspace.
90
+ * Never throws on malformed input. Always provides a usable fallback.
91
+ */
92
+ export function loadPdConfigForPlugin(workspaceDir: string): PluginConfigLoadResult {
93
+ const configPath = getPdConfigPath(workspaceDir);
94
+
95
+ // 1) Config file missing → use defaults
96
+ if (!fs.existsSync(configPath)) {
97
+ const effective = computeEffectivePdConfig(null);
98
+ return {
99
+ ok: true,
100
+ effective,
101
+ source: 'defaults',
102
+ configPath,
103
+ warnings: effective.warnings,
104
+ errors: [],
105
+ };
106
+ }
107
+
108
+ // 2) Read the file
109
+ let raw: string;
110
+ try {
111
+ raw = fs.readFileSync(configPath, 'utf8');
112
+ } catch (err) {
113
+ const message = err instanceof Error ? err.message : String(err);
114
+ const effective = computeEffectivePdConfig(null);
115
+ return {
116
+ ok: false,
117
+ effective,
118
+ source: 'malformed',
119
+ configPath,
120
+ warnings: [],
121
+ errors: [{ path: '', reason: `Failed to read .pd/config.yaml: ${message}`, nextAction: 'Check file permissions for .pd/config.yaml' }],
122
+ };
123
+ }
124
+
125
+ // 3) Parse YAML — treat as unknown (ERR-001)
126
+ let parsed: unknown;
127
+ try {
128
+ parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
129
+ } catch (err) {
130
+ const message = err instanceof Error ? err.message : String(err);
131
+ const effective = computeEffectivePdConfig(null);
132
+ return {
133
+ ok: false,
134
+ effective,
135
+ source: 'malformed',
136
+ configPath,
137
+ warnings: [],
138
+ errors: [{ path: '', reason: `YAML parse error in .pd/config.yaml: ${message}`, nextAction: 'Fix YAML syntax in .pd/config.yaml' }],
139
+ };
140
+ }
141
+
142
+ // 4) Validate via core (ERR-001, ERR-005: no `as` bypasses)
143
+ const validationResult: PdConfigValidationResult = validatePdConfig(parsed);
144
+
145
+ if (!validationResult.ok) {
146
+ const effective = computeEffectivePdConfig(null);
147
+ return {
148
+ ok: false,
149
+ effective,
150
+ source: 'malformed',
151
+ configPath,
152
+ warnings: [],
153
+ errors: validationResult.errors.map(e => ({
154
+ path: e.path,
155
+ reason: e.reason,
156
+ nextAction: e.nextAction,
157
+ })),
158
+ };
159
+ }
160
+
161
+ // 5) Compute effective config
162
+ const effective = computeEffectivePdConfig(validationResult.value);
163
+
164
+ return {
165
+ ok: true,
166
+ effective,
167
+ source: 'user_config',
168
+ configPath,
169
+ warnings: effective.warnings,
170
+ errors: [],
171
+ };
172
+ }
173
+
174
+ // ── Feature Flag from Config ─────────────────────────────────────────────────
175
+
176
+ /**
177
+ * Get a single feature flag's enabled state from .pd/config.yaml.
178
+ * Replaces the old `loadFeatureFlagFromWorkspace` which read .pd/feature-flags.yaml.
179
+ */
180
+ export function loadFeatureFlagFromConfig(
181
+ workspaceDir: string,
182
+ flagId: string,
183
+ logger?: { warn?: (msg: string) => void; info?: (msg: string) => void },
184
+ ): { enabled: boolean; source: string } {
185
+ const result = loadPdConfigForPlugin(workspaceDir);
186
+ const flags = computeFeatureFlagsFromConfig(result.effective);
187
+ const flag = flags.flags[flagId];
188
+
189
+ if (!result.ok) {
190
+ logger?.warn?.(`[PD:Config] Config validation failed: ${result.errors.map(e => e.reason).join('; ')} — using defaults`);
191
+ }
192
+
193
+ return {
194
+ enabled: flag?.enabled ?? false,
195
+ source: result.source,
196
+ };
197
+ }
198
+
199
+ // ── Observer Config Resolution ───────────────────────────────────────────────
200
+
201
+ /**
202
+ * Resolve observer configuration from .pd/config.yaml.
203
+ *
204
+ * Returns structured state:
205
+ * - config_malformed: config file is invalid — no guessing, fail loud
206
+ * - disabled: observer feature flag is off OR agent.enabled=false → no start, no noisy logs
207
+ * - needs_setup: observer enabled but runtime profile missing, API key not set, or unsupported profile type
208
+ * - ready: observer enabled and fully configured (pi-ai with key present)
209
+ * - not_ready: observer enabled, API key present, but runtime availability unknown
210
+ */
211
+ export function resolveObserverConfig(
212
+ workspaceDir: string,
213
+ observerFlagId: string,
214
+ observerAgentName: string,
215
+ _logger?: { warn?: (msg: string) => void; info?: (msg: string) => void; debug?: (msg: string) => void },
216
+ ): ObserverConfigResult {
217
+ const result = loadPdConfigForPlugin(workspaceDir);
218
+
219
+ // 0) Malformed config → fail loud, do NOT swallow as "disabled"
220
+ if (!result.ok) {
221
+ return {
222
+ enabled: false,
223
+ readiness: 'config_malformed',
224
+ source: 'malformed',
225
+ reason: `Config validation failed: ${result.errors.map(e => e.reason).join('; ')}`,
226
+ nextAction: result.errors[0]?.nextAction ?? 'Fix .pd/config.yaml and retry',
227
+ runtimeProfileId: null,
228
+ runtimeProfileType: null,
229
+ apiKeyEnv: null,
230
+ apiKeyPresent: false,
231
+ provider: null,
232
+ model: null,
233
+ timeoutMs: null,
234
+ baseUrl: null,
235
+ configErrors: result.errors,
236
+ };
237
+ }
238
+
239
+ const config = result.effective.config;
240
+
241
+ // 1) Check if the observer feature flag is enabled
242
+ const featureFlag = config.features[observerFlagId];
243
+ if (!featureFlag || !featureFlag.enabled) {
244
+ return {
245
+ enabled: false,
246
+ readiness: 'disabled',
247
+ source: result.source,
248
+ reason: `${observerFlagId} is disabled in .pd/config.yaml`,
249
+ nextAction: `Set features.${observerFlagId}.enabled=true in .pd/config.yaml to enable`,
250
+ runtimeProfileId: null,
251
+ runtimeProfileType: null,
252
+ apiKeyEnv: null,
253
+ apiKeyPresent: false,
254
+ provider: null,
255
+ model: null,
256
+ timeoutMs: null,
257
+ baseUrl: null,
258
+ };
259
+ }
260
+
261
+ // 2) Check if the agent itself is enabled (feature flag ≠ agent.enabled)
262
+ const knownNames: readonly string[] = INTERNAL_AGENT_NAMES;
263
+ if (!knownNames.includes(observerAgentName)) {
264
+ return {
265
+ enabled: false,
266
+ readiness: 'needs_setup',
267
+ source: result.source,
268
+ reason: `Unknown agent name '${observerAgentName}'`,
269
+ nextAction: `Use one of the known agent names: ${INTERNAL_AGENT_NAMES.join(', ')}`,
270
+ runtimeProfileId: null,
271
+ runtimeProfileType: null,
272
+ apiKeyEnv: null,
273
+ apiKeyPresent: false,
274
+ provider: null,
275
+ model: null,
276
+ timeoutMs: null,
277
+ baseUrl: null,
278
+ };
279
+ }
280
+ const agentKey = observerAgentName as InternalAgentName;
281
+ const agentConfig = config.internalAgents.agents[agentKey];
282
+
283
+ // Feature flag on but agent.enabled=false → disabled (not enabled)
284
+ if (!agentConfig || !agentConfig.enabled) {
285
+ return {
286
+ enabled: false,
287
+ readiness: 'disabled',
288
+ source: result.source,
289
+ reason: `${observerFlagId} feature flag is enabled but internalAgents.agents.${observerAgentName}.enabled is false`,
290
+ nextAction: `Set internalAgents.agents.${observerAgentName}.enabled=true in .pd/config.yaml, or disable features.${observerFlagId}`,
291
+ runtimeProfileId: null,
292
+ runtimeProfileType: null,
293
+ apiKeyEnv: null,
294
+ apiKeyPresent: false,
295
+ provider: null,
296
+ model: null,
297
+ timeoutMs: null,
298
+ baseUrl: null,
299
+ };
300
+ }
301
+
302
+ // 3) Find the agent's runtime profile
303
+ const runtimeProfileId = agentConfig.runtimeProfile ?? config.internalAgents.defaultRuntime;
304
+ const profile = config.runtimeProfiles[runtimeProfileId];
305
+
306
+ if (!profile) {
307
+ return {
308
+ enabled: true,
309
+ readiness: 'needs_setup',
310
+ source: result.source,
311
+ reason: `Runtime profile '${runtimeProfileId}' not found in .pd/config.yaml`,
312
+ nextAction: `Add runtime profile '${runtimeProfileId}' to .pd/config.yaml runtimeProfiles`,
313
+ runtimeProfileId,
314
+ runtimeProfileType: null,
315
+ apiKeyEnv: null,
316
+ apiKeyPresent: false,
317
+ provider: null,
318
+ model: null,
319
+ timeoutMs: null,
320
+ baseUrl: null,
321
+ };
322
+ }
323
+
324
+ // 4) For pi-ai profiles, check API key
325
+ if (profile.type === 'pi-ai') {
326
+ const apiKeyEnv = profile.apiKeyEnv ?? null;
327
+ const apiKeyPresent = !!apiKeyEnv && Object.prototype.hasOwnProperty.call(process.env, apiKeyEnv) && !!process.env[apiKeyEnv];
328
+
329
+ if (!apiKeyEnv) {
330
+ return {
331
+ enabled: true,
332
+ readiness: 'needs_setup',
333
+ source: result.source,
334
+ reason: `pi-ai profile '${runtimeProfileId}' missing apiKeyEnv`,
335
+ nextAction: `Add apiKeyEnv to runtime profile '${runtimeProfileId}' in .pd/config.yaml`,
336
+ runtimeProfileId,
337
+ runtimeProfileType: profile.type,
338
+ apiKeyEnv: null,
339
+ apiKeyPresent: false,
340
+ provider: profile.provider ?? null,
341
+ model: profile.model ?? null,
342
+ timeoutMs: profile.timeoutMs ?? null,
343
+ baseUrl: profile.baseUrl ?? null,
344
+ };
345
+ }
346
+
347
+ if (!apiKeyPresent) {
348
+ return {
349
+ enabled: true,
350
+ readiness: 'needs_setup',
351
+ source: result.source,
352
+ reason: `Environment variable '${apiKeyEnv}' is not set or empty`,
353
+ nextAction: `Set the environment variable '${apiKeyEnv}' with a valid API key`,
354
+ runtimeProfileId,
355
+ runtimeProfileType: profile.type,
356
+ apiKeyEnv,
357
+ apiKeyPresent: false,
358
+ provider: profile.provider ?? null,
359
+ model: profile.model ?? null,
360
+ timeoutMs: profile.timeoutMs ?? null,
361
+ baseUrl: profile.baseUrl ?? null,
362
+ };
363
+ }
364
+
365
+ // pi-ai with key present — runtime availability unknown without actual probe
366
+ return {
367
+ enabled: true,
368
+ readiness: 'not_ready',
369
+ source: result.source,
370
+ reason: `pi-ai profile configured with apiKeyEnv='${apiKeyEnv}' (key present); runtime availability unknown`,
371
+ nextAction: 'Run pd runtime probe to verify end-to-end connectivity',
372
+ runtimeProfileId,
373
+ runtimeProfileType: profile.type,
374
+ apiKeyEnv,
375
+ apiKeyPresent: true,
376
+ provider: profile.provider ?? null,
377
+ model: profile.model ?? null,
378
+ timeoutMs: profile.timeoutMs ?? null,
379
+ baseUrl: profile.baseUrl ?? null,
380
+ };
381
+ }
382
+
383
+ // 5) OpenClaw profile — CorrectionObserver does NOT support OpenClaw runtime
384
+ // Mark as needs_setup with nextAction to configure a pi-ai profile
385
+ return {
386
+ enabled: true,
387
+ readiness: 'needs_setup',
388
+ source: result.source,
389
+ reason: `OpenClaw profile '${runtimeProfileId}' is not supported for observer runtime. Observers require a pi-ai profile with an API key.`,
390
+ nextAction: `Configure a pi-ai runtime profile for ${observerAgentName} in .pd/config.yaml (e.g., add a pi-ai profile with provider, model, and apiKeyEnv)`,
391
+ runtimeProfileId,
392
+ runtimeProfileType: profile.type,
393
+ apiKeyEnv: null,
394
+ apiKeyPresent: false,
395
+ provider: profile.provider ?? null,
396
+ model: profile.model ?? null,
397
+ timeoutMs: null,
398
+ baseUrl: null,
399
+ };
400
+ }
@@ -1,8 +1,6 @@
1
- import * as path from 'path';
2
- import * as fs from 'fs';
3
- import * as yaml from 'js-yaml';
4
- import { SqliteConnection, SqliteActivationStateStore, computeEffectiveFlags, DEFAULT_FEATURE_FLAGS, filterPromptActivations, resolvePrincipleFromArtifact } from '@principles/core/runtime-v2';
5
- import type { EffectiveFeatureFlags, ActivatedPrinciple, PromptActivationReaderResult } from '@principles/core/runtime-v2';
1
+ import { SqliteConnection, SqliteActivationStateStore, computeFeatureFlagsFromConfig, filterPromptActivations, resolvePrincipleFromArtifact } from '@principles/core/runtime-v2';
2
+ import type { FeatureFlagsResult, ActivatedPrinciple, PromptActivationReaderResult } from '@principles/core/runtime-v2';
3
+ import { loadPdConfigForPlugin } from './pd-config-loader.js';
6
4
 
7
5
  export { RUNTIME_V2_PRINCIPLE_BUDGET } from '@principles/core/runtime-v2';
8
6
  export type { ActivatedPrinciple, PromptActivationReaderResult };
@@ -11,12 +9,6 @@ export interface PromptActivationReaderDeps {
11
9
  logger?: { warn?: (msg: string) => void; info?: (msg: string) => void; error?: (msg: string) => void };
12
10
  }
13
11
 
14
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
15
-
16
- function isRecord(value: unknown): value is Record<string, unknown> {
17
- return value !== null && typeof value === 'object' && !Array.isArray(value);
18
- }
19
-
20
12
  export class PromptActivationReader {
21
13
  private readonly workspaceDir: string;
22
14
  private readonly deps: PromptActivationReaderDeps;
@@ -103,60 +95,20 @@ export class PromptActivationReader {
103
95
  }
104
96
  }
105
97
 
106
- private loadFeatureFlags(): EffectiveFeatureFlags {
107
- const configPath = path.join(this.workspaceDir, '.pd', 'feature-flags.yaml');
108
-
109
- if (!fs.existsSync(configPath)) {
110
- return computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
111
- }
112
-
113
- let raw: string;
114
- try {
115
- raw = fs.readFileSync(configPath, 'utf8');
116
- } catch (e) {
117
- const msg = e instanceof Error ? e.message : String(e);
118
- this.deps.logger?.warn?.(`[PD:RuntimeV2] Feature flags unreadable: ${msg} — using defaults`);
119
- return computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
120
- }
121
-
122
- let parsed: unknown;
123
- try {
124
- parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
125
- } catch {
126
- this.deps.logger?.warn?.(`[PD:RuntimeV2] Feature flags YAML parse error — using defaults`);
127
- return {
128
- ...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
129
- warnings: ['feature-flags.yaml: YAML parse error, using defaults'],
130
- };
131
- }
132
-
133
- if (!isRecord(parsed)) {
134
- this.deps.logger?.warn?.(`[PD:RuntimeV2] Feature flags not a mapping — using defaults`);
135
- return {
136
- ...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
137
- warnings: ['feature-flags.yaml: expected a mapping, using defaults'],
138
- };
139
- }
140
-
141
- const parsedRecord: Record<string, unknown> = Object.create(null);
142
- const yamlWarnings: string[] = [];
143
- for (const key of Object.keys(parsed)) {
144
- if (DANGEROUS_KEYS.has(key)) {
145
- yamlWarnings.push(`feature-flags.yaml: dangerous key '${key}' rejected`);
146
- continue;
147
- }
148
- if (Object.hasOwn(parsed, key)) {
149
- parsedRecord[key] = parsed[key];
98
+ /**
99
+ * PRI-305/PRI-307: Load feature flags from .pd/config.yaml instead of .pd/feature-flags.yaml.
100
+ * Uses the shared plugin config loader for consistency.
101
+ */
102
+ private loadFeatureFlags(): FeatureFlagsResult {
103
+ const result = loadPdConfigForPlugin(this.workspaceDir);
104
+ const flags = computeFeatureFlagsFromConfig(result.effective);
105
+
106
+ if (!result.ok) {
107
+ for (const err of result.errors) {
108
+ this.deps.logger?.warn?.(`[PD:RuntimeV2] Config error at ${err.path}: ${err.reason}`);
150
109
  }
151
110
  }
152
111
 
153
- const result = computeEffectiveFlags(parsedRecord, DEFAULT_FEATURE_FLAGS, configPath);
154
- if (yamlWarnings.length > 0) {
155
- result.warnings = [...yamlWarnings, ...result.warnings];
156
- for (const w of yamlWarnings) {
157
- this.deps.logger?.warn?.(`[PD:RuntimeV2] ${w}`);
158
- }
159
- }
160
- return result;
112
+ return flags;
161
113
  }
162
114
  }
package/src/hooks/pain.ts CHANGED
@@ -14,6 +14,7 @@ import { resolveWorkspaceDirForRuntimeV2 } from '../utils/workspace-resolver.js'
14
14
  import { PainToPrincipleService, PrincipleTreeLedgerAdapter, type PainDetectedData, type PainEvidenceEntry, MAX_EVIDENCE_ENTRIES, MAX_EVIDENCE_NOTE_CHARS } from '@principles/core/runtime-v2';
15
15
  import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
16
16
  import { sanitizeAssistantText } from './message-sanitize.js';
17
+ import { loadPdConfigForPlugin } from '../core/pd-config-loader.js';
17
18
 
18
19
  /**
19
20
  * Interface for tool parameters to avoid 'any'
@@ -34,12 +35,17 @@ const WRITE_TOOLS = ['write', 'edit', 'apply_patch', 'write_file', 'edit_file',
34
35
 
35
36
  function createPainToPrincipleService(wctx: WorkspaceContext): PainToPrincipleService {
36
37
  const ledgerAdapter = new PrincipleTreeLedgerAdapter({ stateDir: wctx.stateDir });
38
+ // PRI-306: Load .pd/config.yaml and pass effectiveConfig to PainToPrincipleService
39
+ // so createPainSignalBridge uses config-driven runtime binding resolution.
40
+ const configResult = loadPdConfigForPlugin(wctx.workspaceDir);
37
41
  return new PainToPrincipleService({
38
42
  workspaceDir: wctx.workspaceDir,
39
43
  stateDir: wctx.stateDir,
40
44
  ledgerAdapter,
41
45
  owner: 'openclaw-plugin',
42
46
  autoIntakeEnabled: true,
47
+ effectiveConfig: configResult.effective,
48
+ getEnvVar: (name: string) => process.env[name],
43
49
  });
44
50
  }
45
51
 
package/src/index.ts CHANGED
@@ -18,9 +18,7 @@ import type {
18
18
  PluginHookSubagentContext,
19
19
  } from './openclaw-sdk.js';
20
20
  import * as path from 'path';
21
- import * as fs from 'fs';
22
- import * as yaml from 'js-yaml';
23
- import { computeEffectiveFlags, DEFAULT_FEATURE_FLAGS } from '@principles/core/runtime-v2';
21
+ import { loadFeatureFlagFromConfig } from './core/pd-config-loader.js';
24
22
  import { classifyTask } from './core/local-worker-routing.js';
25
23
  import { completeShadowObservation, recordShadowRouting } from './core/shadow-observation-registry.js';
26
24
  import { getCommandDescription } from './i18n/commands.js';
@@ -75,66 +73,20 @@ const pendingShadowObservations = new Map<string, string>();
75
73
  // ── Feature Flag Loader (plugin I/O boundary) ─────────────────────────────
76
74
  // Reads workspace feature-flags.yaml and checks a specific flag.
77
75
  // Returns the flag definition with effective enabled state.
78
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
79
-
80
76
  function isRecord(value: unknown): value is Record<string, unknown> {
81
77
  return value !== null && typeof value === 'object' && !Array.isArray(value);
82
78
  }
83
79
 
80
+ /**
81
+ * PRI-305/PRI-307: Load feature flag from .pd/config.yaml instead of .pd/feature-flags.yaml.
82
+ * Delegates to the shared plugin config loader for consistency.
83
+ */
84
84
  function loadFeatureFlagFromWorkspace(
85
85
  workspaceDir: string,
86
86
  flagId: string,
87
87
  logger?: { warn?: (msg: string) => void; info?: (msg: string) => void },
88
88
  ): { enabled: boolean; source: string } {
89
- const configPath = path.join(workspaceDir, '.pd', 'feature-flags.yaml');
90
-
91
- if (!fs.existsSync(configPath)) {
92
- const flags = computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
93
- const flag = flags.flags[flagId];
94
- return { enabled: flag?.enabled ?? false, source: 'defaults' };
95
- }
96
-
97
- let raw: string;
98
- try {
99
- raw = fs.readFileSync(configPath, 'utf8');
100
- } catch (e) {
101
- const msg = e instanceof Error ? e.message : String(e);
102
- logger?.warn?.(`[PD:FeatureFlags] Feature flags unreadable: ${msg} — using defaults`);
103
- const flags = computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
104
- const flag = flags.flags[flagId];
105
- return { enabled: flag?.enabled ?? false, source: 'defaults' };
106
- }
107
-
108
- let parsed: unknown;
109
- try {
110
- parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
111
- } catch (e) {
112
- const parseMsg = e instanceof Error ? e.message : String(e);
113
- logger?.warn?.(`[PD:FeatureFlags] Feature flags YAML parse error: ${parseMsg} — using defaults`);
114
- const flags = computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
115
- const flag = flags.flags[flagId];
116
- return { enabled: flag?.enabled ?? false, source: 'defaults' };
117
- }
118
-
119
- if (!isRecord(parsed)) {
120
- logger?.warn?.(`[PD:FeatureFlags] Feature flags not a mapping — using defaults`);
121
- const flags = computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
122
- const flag = flags.flags[flagId];
123
- return { enabled: flag?.enabled ?? false, source: 'defaults' };
124
- }
125
-
126
- // parsed is now narrowed to Record<string, unknown> by isRecord guard
127
- const parsedRecord: Record<string, unknown> = Object.create(null);
128
- for (const key of Object.keys(parsed)) {
129
- if (DANGEROUS_KEYS.has(key)) continue;
130
- if (Object.hasOwn(parsed, key)) {
131
- parsedRecord[key] = parsed[key];
132
- }
133
- }
134
-
135
- const flags = computeEffectiveFlags(parsedRecord, DEFAULT_FEATURE_FLAGS, configPath);
136
- const flag = flags.flags[flagId];
137
- return { enabled: flag?.enabled ?? false, source: flags.source };
89
+ return loadFeatureFlagFromConfig(workspaceDir, flagId, logger);
138
90
  }
139
91
 
140
92
  // ── Evolution Worker Startup Gate (shared between index.ts and tests) ───────
@@ -157,7 +109,7 @@ export function shouldStartEvolutionWorker(
157
109
  }
158
110
  const disabledInfo = JSON.stringify({
159
111
  reason: 'mvp_quiet_per_adr0014',
160
- nextAction: 'set evolution_worker.enabled=true in .pd/feature-flags.yaml to enable',
112
+ nextAction: 'set features.evolution_worker.enabled=true in .pd/config.yaml to enable',
161
113
  featureFlag: 'evolution_worker',
162
114
  boundedContext: 'legacy_evolution_worker',
163
115
  flagSource: flag.source,
@@ -181,7 +133,7 @@ export function shouldStartCorrectionObserver(
181
133
  }
182
134
  const disabledInfo = JSON.stringify({
183
135
  reason: 'correction_observer_disabled',
184
- nextAction: 'set correction_observer.enabled=true in .pd/feature-flags.yaml to enable',
136
+ nextAction: 'set features.correction_observer.enabled=true in .pd/config.yaml to enable',
185
137
  featureFlag: 'correction_observer',
186
138
  boundedContext: 'correction_observer_service',
187
139
  flagSource: flag.source,