@principles/pd-cli 1.75.0 → 1.77.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.
- package/dist/commands/config-doctor.d.ts +3 -6
- package/dist/commands/config-doctor.d.ts.map +1 -1
- package/dist/commands/config-doctor.js +30 -31
- package/dist/commands/config-doctor.js.map +1 -1
- package/dist/commands/runtime-features.d.ts +23 -8
- package/dist/commands/runtime-features.d.ts.map +1 -1
- package/dist/commands/runtime-features.js +72 -31
- package/dist/commands/runtime-features.js.map +1 -1
- package/dist/services/config-doctor.d.ts +26 -66
- package/dist/services/config-doctor.d.ts.map +1 -1
- package/dist/services/config-doctor.js +197 -374
- package/dist/services/config-doctor.js.map +1 -1
- package/dist/services/pd-config-loader.d.ts +64 -0
- package/dist/services/pd-config-loader.d.ts.map +1 -0
- package/dist/services/pd-config-loader.js +156 -0
- package/dist/services/pd-config-loader.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/config-doctor.ts +30 -30
- package/src/commands/runtime-features.ts +98 -44
- package/src/services/config-doctor.ts +236 -425
- package/src/services/pd-config-loader.ts +213 -0
- package/tests/commands/config-doctor.test.ts +207 -506
- package/tests/commands/console-launcher-edge-cases.test.ts +421 -0
- package/tests/commands/runtime-features.test.ts +220 -85
- package/tests/services/pd-config-loader.test.ts +479 -0
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* pd config doctor — Discover 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 {
|
|
21
|
-
import {
|
|
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;
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
232
|
-
if (
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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,
|
|
283
|
-
const
|
|
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)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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 (
|
|
308
|
-
|
|
309
|
-
coFlagSource = flags.source;
|
|
277
|
+
else if (!flag.enabled) {
|
|
278
|
+
disabledFlags.push(flag.id);
|
|
310
279
|
}
|
|
311
280
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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:
|
|
335
|
-
apiKeyPresent:
|
|
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:
|
|
324
|
+
reason: agent.reason,
|
|
325
|
+
nextAction: agent.nextAction,
|
|
326
|
+
source: 'config.yaml',
|
|
406
327
|
});
|
|
407
328
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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 (
|
|
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
|
-
|
|
543
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 —
|
|
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;
|