@principles/pd-cli 1.75.0 → 1.76.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.
@@ -1,10 +1,16 @@
1
1
  /**
2
- * PD Config Doctordiscovers and explains PD / OpenClaw configuration state.
2
+ * pd config doctorDiscover and explain PD / OpenClaw configuration state.
3
+ *
4
+ * PRI-305: Cutover to .pd/config.yaml.
5
+ * - Feature flags and internal agent runtime bindings come from .pd/config.yaml
6
+ * - .pd/feature-flags.yaml and .state/workflows.yaml are no longer production inputs
7
+ * - Legacy files are detected and reported as warnings
3
8
  *
4
9
  * PRI-299 MVP UX: provides a single, read-only view of:
5
10
  * - PD workspace config paths (with existence checks)
6
11
  * - OpenClaw config paths (with existence checks)
7
- * - Effective feature flags
12
+ * - Effective feature flags from .pd/config.yaml
13
+ * - Internal agent runtime binding readiness from .pd/config.yaml
8
14
  * - Provider/model/auth status (classified)
9
15
  *
10
16
  * Constraints:
@@ -15,18 +21,10 @@
15
21
  */
16
22
  import * as fs from 'fs';
17
23
  import * as path from 'path';
18
- import yaml from 'js-yaml';
19
24
  import Database from 'better-sqlite3';
20
- import { loadEffectiveFeatureFlags } from './feature-flag-loader.js';
21
- import { FEATURE_FLAGS_CONFIG_FILENAME, FEATURE_FLAGS_CONFIG_DIR } from './feature-flag-loader.js';
25
+ import { loadPdConfig, computeFlagsFromLoadResult, redactLoadResult, getPdConfigPath, } from './pd-config-loader.js';
26
+ import { MVP_CHANNEL_IDS } from '@principles/core/runtime-v2';
22
27
  // ─── Helpers ────────────────────────────────────────────────────────────────
23
- function isRecord(value) {
24
- return value !== null && typeof value === 'object' && !Array.isArray(value);
25
- }
26
- /**
27
- * Resolve the OpenClaw home directory used to look up `openclaw.json` and
28
- * extension state. PD does not own this path — it only reports its existence.
29
- */
30
28
  export function getOpenClawHome() {
31
29
  const home = process.env.HOME
32
30
  || process.env.USERPROFILE
@@ -38,9 +36,6 @@ export function getOpenClawHome() {
38
36
  export function getOpenClawConfigPath() {
39
37
  return path.join(getOpenClawHome(), 'openclaw.json');
40
38
  }
41
- /**
42
- * Build a `ConfigPathEntry` with existence check.
43
- */
44
39
  function pathEntry(p) {
45
40
  return { path: p, exists: fs.existsSync(p) };
46
41
  }
@@ -64,15 +59,7 @@ function classifyText(text) {
64
59
  return null;
65
60
  }
66
61
  const DEFAULT_MAX_ROWS = 50;
67
- const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
68
- /**
69
- * Read the state.db recent provider error signal (best-effort, structural only).
70
- *
71
- * We inspect recent task/run rows in `<workspaceDir>/.pd/state.db` to detect
72
- * `rate_limit` / `auth_missing` / `unavailable` signals from the live pipeline.
73
- * This is a bounded best-effort probe — if the DB is missing or unreadable we
74
- * return `null` and let the doctor fall back to config-only classification.
75
- */
62
+ const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
76
63
  export async function inspectStateDbForProviderSignal(stateDbPath, nowMs = Date.now(), opts = {}) {
77
64
  const maxRows = opts.maxRows ?? DEFAULT_MAX_ROWS;
78
65
  const maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
@@ -91,12 +78,10 @@ export async function inspectStateDbForProviderSignal(stateDbPath, nowMs = Date.
91
78
  };
92
79
  }
93
80
  try {
94
- // Check for tasks table — drift-safe (ERR-026): assert via pragma, not assumption.
95
81
  const tableInfo = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='tasks'").get();
96
82
  if (!tableInfo) {
97
83
  return { signal: null, dbReachable: true, warning: 'state.db has no tasks table — pipeline not initialized yet' };
98
84
  }
99
- // Find columns defensively.
100
85
  const taskCols = db.prepare("PRAGMA table_info(tasks)").all();
101
86
  const taskColNames = new Set(taskCols.map((c) => c.name));
102
87
  const hasErrorMessage = taskColNames.has('error_message');
@@ -109,7 +94,6 @@ export async function inspectStateDbForProviderSignal(stateDbPath, nowMs = Date.
109
94
  if (!hasErrorMessage && !hasLastErrorMessage) {
110
95
  return { signal: null, dbReachable: true, warning: 'state.db tasks table has no error_message column — provider signal unavailable' };
111
96
  }
112
- // Build a bounded scan: read up to maxRows recent failed tasks.
113
97
  const errorCol = hasErrorMessage ? 'error_message' : 'last_error_message';
114
98
  const tsCol = hasUpdatedAt ? 'updated_at' : hasLastErrorAt ? 'last_error_at' : hasCreatedAt ? 'created_at' : null;
115
99
  const tsExpr = tsCol ? `, ${tsCol} AS ts` : '';
@@ -126,7 +110,6 @@ export async function inspectStateDbForProviderSignal(stateDbPath, nowMs = Date.
126
110
  mostRecentObservedAt = row.ts;
127
111
  }
128
112
  }
129
- // Also check for explicit error_category/failure_category columns if present.
130
113
  if (hasErrorCategory || hasFailureCategory) {
131
114
  const catCol = hasErrorCategory ? 'error_category' : 'failure_category';
132
115
  const catRows = db.prepare(`SELECT ${catCol} AS cat${tsCol ? `, ${tsCol} AS ts` : ''} FROM tasks WHERE ${catCol} IS NOT NULL ${tsCol ? `AND ${tsCol} >= ?` : ''} ORDER BY rowid DESC LIMIT ?`).all(...(tsCol ? [nowMs - maxAgeMs] : []), maxRows);
@@ -163,392 +146,251 @@ export async function inspectStateDbForProviderSignal(stateDbPath, nowMs = Date.
163
146
  catch { /* best effort */ }
164
147
  }
165
148
  }
166
- const DIAGNOSTIC_FUNNEL_ID = 'pd-runtime-v2-diagnosis';
167
- export async function resolveProviderConfigFromWorkflows(stateDir, opts = {}) {
168
- const workflowsPath = path.join(stateDir, 'workflows.yaml');
169
- if (!fs.existsSync(workflowsPath)) {
170
- // Fall back to CLI flag values if provided.
171
- if (opts.cliProvider || opts.cliModel || opts.cliApiKeyEnv) {
172
- return {
173
- provider: opts.cliProvider ?? null,
174
- model: opts.cliModel ?? null,
175
- apiKeyEnv: opts.cliApiKeyEnv ?? null,
176
- baseUrl: opts.cliBaseUrl ?? null,
177
- source: 'cli_flag',
178
- workflowsFound: false,
179
- };
180
- }
181
- return {
182
- provider: null,
183
- model: null,
184
- apiKeyEnv: null,
185
- baseUrl: null,
186
- source: 'missing',
187
- workflowsFound: false,
188
- };
189
- }
190
- let raw;
191
- try {
192
- raw = fs.readFileSync(workflowsPath, 'utf8');
193
- }
194
- catch (err) {
195
- return {
196
- provider: null,
197
- model: null,
198
- apiKeyEnv: null,
199
- baseUrl: null,
200
- source: 'workflows.yaml',
201
- workflowsFound: true,
202
- parseWarning: `workflows.yaml read failed: ${err instanceof Error ? err.message : String(err)}`,
203
- };
204
- }
205
- let parsed;
206
- try {
207
- parsed = yaml.load(raw);
208
- }
209
- catch (err) {
149
+ // ─── Internal Agent Diagnostics from .pd/config.yaml ─────────────────────────
150
+ function diagnoseInternalAgent(agent, profile) {
151
+ if (!agent.enabled) {
210
152
  return {
153
+ name: agent.name,
154
+ enabled: false,
155
+ runtimeProfileId: agent.runtimeProfileId,
156
+ runtimeProfileLabel: agent.runtimeProfileLabel,
157
+ readiness: 'disabled',
211
158
  provider: null,
212
159
  model: null,
213
160
  apiKeyEnv: null,
214
- baseUrl: null,
215
- source: 'workflows.yaml',
216
- workflowsFound: true,
217
- parseWarning: `workflows.yaml parse error: ${err instanceof Error ? err.message : String(err)}`,
161
+ apiKeyPresent: false,
162
+ reason: `${agent.name} is disabled in .pd/config.yaml`,
163
+ nextAction: `Set internalAgents.agents.${agent.name}.enabled=true in .pd/config.yaml to enable`,
218
164
  };
219
165
  }
220
- if (!isRecord(parsed)) {
166
+ // Agent is enabled — check profile readiness
167
+ if (!profile) {
221
168
  return {
169
+ name: agent.name,
170
+ enabled: true,
171
+ runtimeProfileId: agent.runtimeProfileId,
172
+ runtimeProfileLabel: agent.runtimeProfileLabel,
173
+ readiness: 'needs_setup',
222
174
  provider: null,
223
175
  model: null,
224
176
  apiKeyEnv: null,
225
- baseUrl: null,
226
- source: 'workflows.yaml',
227
- workflowsFound: true,
228
- parseWarning: 'workflows.yaml root is not an object',
177
+ apiKeyPresent: false,
178
+ reason: `Runtime profile '${agent.runtimeProfileId}' not found in .pd/config.yaml`,
179
+ nextAction: `Add runtime profile '${agent.runtimeProfileId}' to .pd/config.yaml runtimeProfiles, or change the agent's runtimeProfile reference`,
229
180
  };
230
181
  }
231
- const funnelsRaw = parsed.funnels;
232
- if (!Array.isArray(funnelsRaw)) {
182
+ // Check apiKeyEnv for pi-ai profiles
183
+ if (profile.type === 'pi-ai') {
184
+ const apiKeyEnv = profile.apiKeyEnv ?? null;
185
+ const apiKeyPresent = !!apiKeyEnv && Object.prototype.hasOwnProperty.call(process.env, apiKeyEnv) && !!process.env[apiKeyEnv];
186
+ if (!apiKeyEnv) {
187
+ return {
188
+ name: agent.name,
189
+ enabled: true,
190
+ runtimeProfileId: agent.runtimeProfileId,
191
+ runtimeProfileLabel: agent.runtimeProfileLabel,
192
+ readiness: 'needs_setup',
193
+ provider: null,
194
+ model: null,
195
+ apiKeyEnv: null,
196
+ apiKeyPresent: false,
197
+ reason: `pi-ai profile '${profile.id}' missing apiKeyEnv`,
198
+ nextAction: `Add apiKeyEnv to runtime profile '${profile.id}' in .pd/config.yaml`,
199
+ };
200
+ }
201
+ if (!apiKeyPresent) {
202
+ return {
203
+ name: agent.name,
204
+ enabled: true,
205
+ runtimeProfileId: agent.runtimeProfileId,
206
+ runtimeProfileLabel: agent.runtimeProfileLabel,
207
+ readiness: 'needs_setup',
208
+ provider: null,
209
+ model: null,
210
+ apiKeyEnv,
211
+ apiKeyPresent: false,
212
+ reason: `Environment variable '${apiKeyEnv}' is not set or empty`,
213
+ nextAction: `Set the environment variable '${apiKeyEnv}' with a valid API key`,
214
+ };
215
+ }
216
+ // pi-ai with key present — check state.db for signals
233
217
  return {
218
+ name: agent.name,
219
+ enabled: true,
220
+ runtimeProfileId: agent.runtimeProfileId,
221
+ runtimeProfileLabel: agent.runtimeProfileLabel,
222
+ readiness: 'not_ready', // runtime availability unknown without actual probe
234
223
  provider: null,
235
224
  model: null,
236
- apiKeyEnv: null,
237
- baseUrl: null,
238
- source: 'workflows.yaml',
239
- workflowsFound: true,
240
- parseWarning: 'workflows.yaml: funnels is not an array',
225
+ apiKeyEnv,
226
+ apiKeyPresent: true,
227
+ reason: `pi-ai profile configured with apiKeyEnv='${apiKeyEnv}' (key present); runtime availability unknown`,
228
+ nextAction: 'Run pd runtime probe to verify end-to-end connectivity',
241
229
  };
242
230
  }
243
- let policy = null;
244
- for (const f of funnelsRaw) {
245
- if (!isRecord(f))
246
- continue;
247
- if (f.workflowId === DIAGNOSTIC_FUNNEL_ID && isRecord(f.policy)) {
248
- const { policy: candidate } = f;
249
- if (isRecord(candidate)) {
250
- policy = candidate;
251
- break;
252
- }
253
- }
254
- }
255
- if (policy === null) {
231
+ // OpenClaw profile
232
+ if (profile.readiness === 'needs_setup') {
256
233
  return {
234
+ name: agent.name,
235
+ enabled: true,
236
+ runtimeProfileId: agent.runtimeProfileId,
237
+ runtimeProfileLabel: agent.runtimeProfileLabel,
238
+ readiness: 'needs_setup',
257
239
  provider: null,
258
240
  model: null,
259
241
  apiKeyEnv: null,
260
- baseUrl: null,
261
- source: 'workflows.yaml',
262
- workflowsFound: true,
263
- parseWarning: `workflows.yaml: funnel '${DIAGNOSTIC_FUNNEL_ID}' not found`,
242
+ apiKeyPresent: false,
243
+ reason: `OpenClaw profile '${profile.id}' needs setup (missing provider/model)`,
244
+ nextAction: `Configure provider and model in runtime profile '${profile.id}' in .pd/config.yaml`,
264
245
  };
265
246
  }
266
- const provider = typeof policy.provider === 'string' ? policy.provider : null;
267
- const model = typeof policy.model === 'string' ? policy.model : null;
268
- const apiKeyEnv = typeof policy.apiKeyEnv === 'string' ? policy.apiKeyEnv : null;
269
- const baseUrl = typeof policy.baseUrl === 'string' ? policy.baseUrl : null;
270
247
  return {
271
- provider: provider || opts.cliProvider || null,
272
- model: model || opts.cliModel || null,
273
- apiKeyEnv: apiKeyEnv || opts.cliApiKeyEnv || null,
274
- baseUrl: baseUrl || opts.cliBaseUrl || null,
275
- source: 'workflows.yaml',
276
- workflowsFound: true,
248
+ name: agent.name,
249
+ enabled: true,
250
+ runtimeProfileId: agent.runtimeProfileId,
251
+ runtimeProfileLabel: agent.runtimeProfileLabel,
252
+ readiness: 'ready',
253
+ provider: null,
254
+ model: null,
255
+ apiKeyEnv: null,
256
+ apiKeyPresent: false,
257
+ reason: `OpenClaw profile '${profile.id}' is configured and ready`,
258
+ nextAction: 'No action required',
277
259
  };
278
260
  }
279
- const MVP_CHANNELS = new Set(['prompt', 'code_tool_hook', 'defer_archive']);
280
261
  export async function buildDoctorOutput(input) {
281
262
  const workspaceDir = path.resolve(input.workspaceDir);
282
- const pdDir = path.join(workspaceDir, FEATURE_FLAGS_CONFIG_DIR);
283
- const featureFlagsPath = path.join(pdDir, FEATURE_FLAGS_CONFIG_FILENAME);
284
- const workflowsPath = path.join(workspaceDir, '.state', 'workflows.yaml');
263
+ const pdDir = path.join(workspaceDir, '.pd');
264
+ const configYamlPath = getPdConfigPath(workspaceDir);
285
265
  const stateDbPath = path.join(pdDir, 'state.db');
286
- // 1) Feature flags
287
- let enabledMvpChannels = [];
288
- let disabledFlags = [];
289
- let flagSource;
290
- let flagWarnings;
291
- let hasFeatureFlagsError = false;
292
- let featureFlagsErrorMessage = '';
293
- let correctionObserverEnabled = true;
294
- let coFlagSource = 'defaults';
295
- try {
296
- const flags = loadEffectiveFeatureFlags(workspaceDir);
297
- flagSource = flags.source;
298
- flagWarnings = [...flags.warnings];
299
- for (const flag of Object.values(flags.flags)) {
300
- if (flag.enabled && MVP_CHANNELS.has(flag.id)) {
301
- enabledMvpChannels.push(flag.id);
302
- }
303
- else if (!flag.enabled) {
304
- disabledFlags.push(flag.id);
305
- }
266
+ // 1) Load PD config from .pd/config.yaml
267
+ const loadResult = loadPdConfig(workspaceDir);
268
+ const flags = computeFlagsFromLoadResult(loadResult);
269
+ const redacted = redactLoadResult(loadResult);
270
+ // 2) Feature flag summary
271
+ const enabledMvpChannels = [];
272
+ const disabledFlags = [];
273
+ for (const flag of Object.values(flags.flags)) {
274
+ if (flag.enabled && MVP_CHANNEL_IDS.includes(flag.id)) {
275
+ enabledMvpChannels.push(flag.id);
306
276
  }
307
- if (flags.flags && flags.flags.correction_observer) {
308
- correctionObserverEnabled = flags.flags.correction_observer.enabled;
309
- coFlagSource = flags.source;
277
+ else if (!flag.enabled) {
278
+ disabledFlags.push(flag.id);
310
279
  }
311
280
  }
312
- catch (err) {
313
- hasFeatureFlagsError = true;
314
- featureFlagsErrorMessage = err instanceof Error ? err.message : String(err);
315
- flagSource = 'unavailable';
316
- flagWarnings = [`feature flags unavailable: ${featureFlagsErrorMessage}`];
317
- coFlagSource = 'unavailable';
281
+ const featureFlags = {
282
+ source: loadResult.ok ? loadResult.source : 'malformed',
283
+ configPath: loadResult.configPath,
284
+ enabledMvpChannels,
285
+ disabledFlags,
286
+ warnings: flags.warnings,
287
+ };
288
+ // 3) Internal agent diagnostics from .pd/config.yaml
289
+ const profileMap = new Map();
290
+ for (const p of redacted.runtimeProfiles) {
291
+ profileMap.set(p.id, p);
292
+ }
293
+ const internalAgents = [];
294
+ for (const agent of redacted.agents) {
295
+ const profile = profileMap.get(agent.runtimeProfileId);
296
+ internalAgents.push(diagnoseInternalAgent(agent, profile));
318
297
  }
319
- // 2) Provider config from workflows.yaml (or CLI override)
320
- const providerCfg = await resolveProviderConfigFromWorkflows(path.join(workspaceDir, '.state'), {
321
- cliProvider: input.cliProvider,
322
- cliModel: input.cliModel,
323
- cliApiKeyEnv: input.cliApiKeyEnv,
324
- cliBaseUrl: input.cliBaseUrl,
325
- });
326
- // 3) Build the provider health entry. NEVER leak env var value.
298
+ // 4) Provider health — derived from internal agents that are enabled
327
299
  const providerHealth = [];
328
300
  const warnings = [];
329
301
  const nextActions = [];
330
- if (providerCfg.source === 'missing' && !input.cliProvider) {
302
+ // Add config-level warnings
303
+ warnings.push(...loadResult.warnings);
304
+ warnings.push(...flags.warnings);
305
+ if (!loadResult.ok) {
306
+ for (const err of loadResult.errors) {
307
+ warnings.push(`Config error at ${err.path}: ${err.reason}`);
308
+ }
309
+ nextActions.push(loadResult.errors[0]?.nextAction ?? 'Fix .pd/config.yaml and retry');
310
+ }
311
+ // Check for enabled agents that need setup
312
+ const needsSetupAgents = internalAgents.filter(a => a.enabled && (a.readiness === 'needs_setup' || a.readiness === 'not_ready'));
313
+ for (const agent of needsSetupAgents) {
314
+ // not_ready = key present but not probed → needs_probe (degraded), NOT auth_missing (failed)
315
+ const classification = agent.readiness === 'needs_setup'
316
+ ? 'config_missing'
317
+ : 'needs_probe';
331
318
  providerHealth.push({
332
319
  provider: null,
333
320
  model: null,
334
- apiKeyEnv: null,
335
- apiKeyPresent: false,
336
- classification: 'config_missing',
337
- reason: 'No provider configured (workflows.yaml funnel policy missing and no CLI override)',
338
- nextAction: 'Configure provider/model/apiKeyEnv in workflows.yaml pd-runtime-v2-diagnosis policy, or pass --provider/--model/--apiKeyEnv flags',
339
- source: providerCfg.source,
340
- });
341
- warnings.push('No provider configured for the diagnostic funnel');
342
- nextActions.push('Configure provider/model/apiKeyEnv in workflows.yaml or pass CLI flags');
343
- }
344
- else {
345
- const apiKeyEnvName = providerCfg.apiKeyEnv;
346
- const apiKeyPresent = !!apiKeyEnvName && Object.prototype.hasOwnProperty.call(process.env, apiKeyEnvName) && !!process.env[apiKeyEnvName];
347
- let classification;
348
- let reason;
349
- let nextAction;
350
- if (!providerCfg.provider || !providerCfg.model) {
351
- classification = 'config_missing';
352
- reason = !providerCfg.provider
353
- ? 'Provider not configured'
354
- : 'Model not configured';
355
- nextAction = 'Set provider and model in workflows.yaml funnel policy or pass --provider/--model flags';
356
- }
357
- else if (!apiKeyEnvName) {
358
- classification = 'auth_missing';
359
- reason = 'apiKeyEnv is not set in workflows.yaml';
360
- nextAction = "Add 'apiKeyEnv: <ENV_VAR_NAME>' to workflows.yaml funnel policy and ensure the env var holds a valid key";
361
- }
362
- else if (!apiKeyPresent) {
363
- classification = 'auth_missing';
364
- reason = `Environment variable '${apiKeyEnvName}' is not set or empty`;
365
- nextAction = `Set the environment variable '${apiKeyEnvName}' with a valid API key, then retry`;
366
- }
367
- else {
368
- // Config is well-formed and the key is present — check state.db for rate-limit / unavailable signals.
369
- const signalResult = await inspectStateDbForProviderSignal(stateDbPath);
370
- if (signalResult.warning) {
371
- warnings.push(signalResult.warning);
372
- }
373
- if (signalResult.signal) {
374
- const { classification: cls, reason: rsn } = signalResult.signal;
375
- classification = cls;
376
- reason = rsn;
377
- switch (classification) {
378
- case 'rate_limit':
379
- nextAction = `Wait for the provider's rate limit to reset, or reduce request rate. Recent error in state.db.`;
380
- break;
381
- case 'auth_missing':
382
- nextAction = `The env var is set, but the provider rejected the call. Verify the key value is current and has access to model '${providerCfg.model}'.`;
383
- break;
384
- case 'unavailable':
385
- nextAction = `Provider/model temporarily unavailable. Try a different model or retry later. Check provider status page.`;
386
- break;
387
- default:
388
- nextAction = 'Inspect recent task errors in state.db for details.';
389
- }
390
- }
391
- else {
392
- classification = 'healthy';
393
- reason = 'Provider, model, and apiKeyEnv are configured; env var is set; no recent error signals';
394
- nextAction = 'Run pd diagnose run to validate end-to-end provider connectivity';
395
- }
396
- }
397
- providerHealth.push({
398
- provider: providerCfg.provider,
399
- model: providerCfg.model,
400
- apiKeyEnv: apiKeyEnvName,
401
- apiKeyPresent,
321
+ apiKeyEnv: agent.apiKeyEnv,
322
+ apiKeyPresent: agent.apiKeyPresent,
402
323
  classification,
403
- reason,
404
- nextAction,
405
- source: providerCfg.source,
324
+ reason: agent.reason,
325
+ nextAction: agent.nextAction,
326
+ source: 'config.yaml',
406
327
  });
407
328
  }
408
- if (providerCfg.parseWarning) {
409
- warnings.push(providerCfg.parseWarning);
410
- if (!providerCfg.workflowsFound) {
411
- nextActions.push('Create workflows.yaml in <workspace>/.state with a pd-runtime-v2-diagnosis funnel policy');
329
+ // Check state.db for rate-limit / unavailable signals for enabled agents
330
+ const readyAgents = internalAgents.filter(a => a.enabled && a.readiness === 'ready');
331
+ if (readyAgents.length > 0) {
332
+ const signalResult = await inspectStateDbForProviderSignal(stateDbPath);
333
+ if (signalResult.warning) {
334
+ warnings.push(signalResult.warning);
412
335
  }
413
- else {
414
- nextActions.push('Fix workflows.yaml parse error see warnings for details');
415
- }
416
- }
417
- // 4) Aggregate feature-flag warnings
418
- for (const w of flagWarnings) {
419
- warnings.push(w);
420
- }
421
- if (flagWarnings.length > 0 && !hasFeatureFlagsError) {
422
- nextActions.push('Review .pd/feature-flags.yaml see warnings for details');
423
- }
424
- if (hasFeatureFlagsError) {
425
- warnings.push(`feature flags unavailable: ${featureFlagsErrorMessage}`);
426
- nextActions.push(`Check that ${featureFlagsPath} is a readable file, not a directory.`);
427
- nextActions.push('Re-run npx create-principles-disciple if the config was generated incorrectly.');
428
- }
429
- // 4.5) CorrectionObserver Diagnostics
430
- let coStatus;
431
- let coConfigSource;
432
- let coProvider = null;
433
- let coModel = null;
434
- let coApiKeyEnv = null;
435
- let coApiKeyPresent = false;
436
- let coReason;
437
- let coNextAction;
438
- if (!correctionObserverEnabled) {
439
- coStatus = 'disabled';
440
- coConfigSource = 'missing';
441
- coProvider = null;
442
- coModel = null;
443
- coApiKeyEnv = null;
444
- coApiKeyPresent = false;
445
- coReason = 'CorrectionObserver is disabled via feature flags';
446
- coNextAction = 'Set correction_observer.enabled=true in .pd/feature-flags.yaml to enable it';
447
- }
448
- else {
449
- const coWorkflowsPath = path.join(workspaceDir, '.state', 'workflows.yaml');
450
- let coWorkflowsFound = false;
451
- let coWorkflowsParseError = false;
452
- let coWorkflowsParseErrorMessage = '';
453
- let coWorkflowPolicy = null;
454
- if (fs.existsSync(coWorkflowsPath)) {
455
- coWorkflowsFound = true;
456
- try {
457
- const raw = fs.readFileSync(coWorkflowsPath, 'utf8');
458
- const parsed = yaml.load(raw);
459
- if (isRecord(parsed)) {
460
- const funnelsRaw = parsed.funnels;
461
- if (Array.isArray(funnelsRaw)) {
462
- for (const f of funnelsRaw) {
463
- if (isRecord(f) && f.workflowId === 'pd-correction-observer' && isRecord(f.policy)) {
464
- coWorkflowPolicy = f.policy;
465
- break;
466
- }
467
- }
468
- }
469
- }
470
- }
471
- catch (err) {
472
- coWorkflowsParseError = true;
473
- coWorkflowsParseErrorMessage = err instanceof Error ? err.message : String(err);
474
- }
475
- }
476
- if (coWorkflowsParseError) {
477
- coStatus = 'unavailable';
478
- coConfigSource = 'unavailable';
479
- coReason = `workflows.yaml parse failure: ${coWorkflowsParseErrorMessage}`;
480
- coNextAction = 'Fix workflows.yaml syntax or file access permissions';
481
- }
482
- else if (coWorkflowsFound && coWorkflowPolicy && coWorkflowPolicy.runtimeKind === 'pi-ai') {
483
- coConfigSource = 'workflows.yaml';
484
- coProvider = typeof coWorkflowPolicy.provider === 'string' ? coWorkflowPolicy.provider : null;
485
- coModel = typeof coWorkflowPolicy.model === 'string' ? coWorkflowPolicy.model : null;
486
- coApiKeyEnv = typeof coWorkflowPolicy.apiKeyEnv === 'string' ? coWorkflowPolicy.apiKeyEnv : null;
487
- coApiKeyPresent = !!coApiKeyEnv && Object.prototype.hasOwnProperty.call(process.env, coApiKeyEnv) && !!process.env[coApiKeyEnv];
488
- if (!coProvider || !coModel) {
489
- coStatus = 'config_missing';
490
- coReason = !coProvider ? 'Provider not configured in workflows.yaml policy' : 'Model not configured in workflows.yaml policy';
491
- coNextAction = 'Set provider and model in pd-correction-observer policy in workflows.yaml';
492
- }
493
- else if (!coApiKeyEnv) {
494
- coStatus = 'auth_missing';
495
- coReason = 'apiKeyEnv is not set in workflows.yaml pd-correction-observer policy';
496
- coNextAction = "Add 'apiKeyEnv: <ENV_VAR_NAME>' to workflows.yaml pd-correction-observer policy and ensure the env var holds a valid key";
497
- }
498
- else if (!coApiKeyPresent) {
499
- coStatus = 'auth_missing';
500
- coReason = `Environment variable '${coApiKeyEnv}' is not set or empty`;
501
- coNextAction = `Set the environment variable '${coApiKeyEnv}' with a valid API key, or disable correction_observer in feature flags`;
502
- }
503
- else {
504
- coStatus = 'configured';
505
- coReason = 'CorrectionObserver is configured and ready via workflows.yaml';
506
- coNextAction = 'No action required; correction observer is active';
507
- }
336
+ if (signalResult.signal) {
337
+ const { classification: cls, reason: rsn } = signalResult.signal;
338
+ providerHealth.push({
339
+ provider: null,
340
+ model: null,
341
+ apiKeyEnv: null,
342
+ apiKeyPresent: false,
343
+ classification: cls,
344
+ reason: rsn,
345
+ nextAction: cls === 'rate_limit'
346
+ ? "Wait for the provider's rate limit to reset, or reduce request rate"
347
+ : 'Provider/model temporarily unavailable. Try a different model or retry later.',
348
+ source: 'state.db',
349
+ });
508
350
  }
509
351
  else {
510
- coConfigSource = 'env';
511
- coProvider = process.env.PD_CORRECTION_PROVIDER || 'anthropic';
512
- coModel = process.env.PD_CORRECTION_MODEL || 'anthropic/claude-3-5-sonnet';
513
- coApiKeyEnv = process.env.PD_CORRECTION_API_KEY_ENV || 'ANTHROPIC_API_KEY';
514
- coApiKeyPresent = !!coApiKeyEnv && Object.prototype.hasOwnProperty.call(process.env, coApiKeyEnv) && !!process.env[coApiKeyEnv];
515
- if (!coApiKeyPresent) {
516
- coStatus = 'auth_missing';
517
- coReason = `Environment variable '${coApiKeyEnv}' is not set or empty`;
518
- coNextAction = `Set the environment variable '${coApiKeyEnv}' with a valid API key, or configure workflows.yaml, or disable correction_observer in feature flags`;
519
- }
520
- else {
521
- coStatus = 'configured';
522
- coReason = 'CorrectionObserver is configured and ready via env overrides/defaults';
523
- coNextAction = 'No action required; correction observer is active';
524
- }
352
+ providerHealth.push({
353
+ provider: null,
354
+ model: null,
355
+ apiKeyEnv: null,
356
+ apiKeyPresent: false,
357
+ classification: 'healthy',
358
+ reason: 'Config is valid; no recent error signals in state.db',
359
+ nextAction: 'Run pd runtime probe to verify end-to-end connectivity',
360
+ source: 'config.yaml',
361
+ });
525
362
  }
526
363
  }
527
364
  // 5) Compute overall status
528
365
  let status = 'ok';
529
366
  const classifications = providerHealth.map((p) => p.classification);
530
- if (classifications.includes('rate_limit') || classifications.includes('unavailable')) {
367
+ if (classifications.includes('rate_limit') || classifications.includes('unavailable') || classifications.includes('needs_probe')) {
531
368
  status = 'degraded';
532
369
  }
533
370
  if (classifications.includes('auth_missing') || classifications.includes('config_missing')) {
534
371
  status = 'failed';
535
372
  }
536
- if ((warnings.length > 0 || hasFeatureFlagsError) && status === 'ok') {
373
+ if (!loadResult.ok) {
374
+ status = 'failed';
375
+ }
376
+ if ((warnings.length > 0) && status === 'ok') {
537
377
  status = 'degraded';
538
378
  }
539
379
  // 6) Reason
540
380
  let reason;
541
381
  if (status === 'failed') {
542
- const failed = providerHealth.filter((p) => p.classification === 'auth_missing' || p.classification === 'config_missing');
543
- if (failed.length > 0) {
544
- reason = `Provider auth/config missing: ${failed.map((p) => p.classification).join(', ')}`;
382
+ if (!loadResult.ok) {
383
+ reason = `Config validation failed: ${loadResult.errors.map(e => e.reason).join('; ')}`;
545
384
  }
546
385
  else {
547
- reason = 'Configuration is missing required fields';
386
+ const failed = providerHealth.filter((p) => p.classification === 'auth_missing' || p.classification === 'config_missing');
387
+ reason = failed.length > 0
388
+ ? `Provider auth/config missing: ${failed.map((p) => p.classification).join(', ')}`
389
+ : 'Configuration is missing required fields';
548
390
  }
549
391
  }
550
392
  else if (status === 'degraded') {
551
- const degraded = providerHealth.filter((p) => p.classification === 'rate_limit' || p.classification === 'unavailable');
393
+ const degraded = providerHealth.filter((p) => p.classification === 'rate_limit' || p.classification === 'unavailable' || p.classification === 'needs_probe');
552
394
  if (degraded.length > 0) {
553
395
  reason = `Provider connectivity degraded: ${degraded.map((p) => p.classification).join(', ')}`;
554
396
  }
@@ -556,45 +398,26 @@ export async function buildDoctorOutput(input) {
556
398
  reason = `Config warnings present: ${warnings.length} item(s)`;
557
399
  }
558
400
  }
559
- // 7) Build path entries
401
+ // 7) Build output
560
402
  const out = {
561
403
  status,
562
404
  workspaceDir,
563
405
  pdConfigPaths: {
564
406
  workspaceDir: pathEntry(workspaceDir),
565
407
  pdDir: pathEntry(pdDir),
566
- featureFlags: pathEntry(featureFlagsPath),
567
- workflowsYaml: pathEntry(workflowsPath),
408
+ configYaml: { ...pathEntry(configYamlPath), parseable: loadResult.ok || !fs.existsSync(configYamlPath) ? true : false },
568
409
  stateDb: pathEntry(stateDbPath),
569
410
  },
570
411
  openclawConfigPaths: {
571
412
  openclawHome: pathEntry(getOpenClawHome()),
572
413
  openclawConfig: pathEntry(getOpenClawConfigPath()),
573
414
  },
574
- featureFlags: {
575
- source: flagSource,
576
- configPath: featureFlagsPath,
577
- enabledMvpChannels,
578
- disabledFlags,
579
- warnings: flagWarnings,
580
- },
415
+ featureFlags,
416
+ internalAgents,
581
417
  providerHealth,
582
- internalAgents: {
583
- correctionObserver: {
584
- enabled: correctionObserverEnabled,
585
- flagSource: coFlagSource,
586
- status: coStatus,
587
- configSource: coConfigSource,
588
- provider: coProvider,
589
- model: coModel,
590
- apiKeyEnv: coApiKeyEnv,
591
- apiKeyPresent: coApiKeyPresent,
592
- reason: coReason,
593
- nextAction: coNextAction,
594
- },
595
- },
596
418
  warnings,
597
- nextActions: nextActions.length > 0 ? nextActions : ['All checks passed — provider is configured and reachable'],
419
+ nextActions: nextActions.length > 0 ? nextActions : ['All checks passed — configuration is valid'],
420
+ legacyFilesDetected: loadResult.legacyFilesDetected,
598
421
  };
599
422
  if (reason)
600
423
  out.reason = reason;