@principles/pd-cli 1.74.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.
- 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/console.d.ts +18 -0
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +439 -0
- package/dist/commands/console.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/index.js +51 -15
- package/dist/index.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/console-launcher.d.ts +110 -0
- package/dist/services/console-launcher.d.ts.map +1 -0
- package/dist/services/console-launcher.js +282 -0
- package/dist/services/console-launcher.js.map +1 -0
- 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/console.ts +445 -1
- package/src/commands/runtime-features.ts +98 -44
- package/src/index.ts +55 -16
- package/src/services/config-doctor.ts +236 -425
- package/src/services/console-launcher.ts +373 -0
- package/src/services/pd-config-loader.ts +213 -0
- package/tests/commands/config-doctor.test.ts +207 -506
- package/tests/commands/console-open.test.ts +773 -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:
|
|
@@ -16,11 +22,19 @@
|
|
|
16
22
|
|
|
17
23
|
import * as fs from 'fs';
|
|
18
24
|
import * as path from 'path';
|
|
19
|
-
import yaml from 'js-yaml';
|
|
20
25
|
import Database from 'better-sqlite3';
|
|
21
26
|
import type { Database as BetterSqliteDatabase } from 'better-sqlite3';
|
|
22
|
-
import {
|
|
23
|
-
|
|
27
|
+
import {
|
|
28
|
+
loadPdConfig,
|
|
29
|
+
computeFlagsFromLoadResult,
|
|
30
|
+
redactLoadResult,
|
|
31
|
+
getPdConfigPath,
|
|
32
|
+
} from './pd-config-loader.js';
|
|
33
|
+
import { MVP_CHANNEL_IDS } from '@principles/core/runtime-v2';
|
|
34
|
+
import type {
|
|
35
|
+
RedactedAgentSummary,
|
|
36
|
+
RedactedRuntimeProfileSummary,
|
|
37
|
+
} from '@principles/core/runtime-v2';
|
|
24
38
|
|
|
25
39
|
// ─── Public types ────────────────────────────────────────────────────────────
|
|
26
40
|
|
|
@@ -28,6 +42,7 @@ export type DoctorClassification =
|
|
|
28
42
|
| 'healthy'
|
|
29
43
|
| 'config_missing'
|
|
30
44
|
| 'auth_missing'
|
|
45
|
+
| 'needs_probe'
|
|
31
46
|
| 'rate_limit'
|
|
32
47
|
| 'unavailable'
|
|
33
48
|
| 'parse_failure'
|
|
@@ -38,7 +53,6 @@ export type DoctorStatus = 'ok' | 'degraded' | 'failed';
|
|
|
38
53
|
export interface ConfigPathEntry {
|
|
39
54
|
path: string;
|
|
40
55
|
exists: boolean;
|
|
41
|
-
/** Optional structural check (file is parseable JSON/YAML, etc.) */
|
|
42
56
|
parseable?: boolean;
|
|
43
57
|
}
|
|
44
58
|
|
|
@@ -46,12 +60,10 @@ export interface ProviderHealthEntry {
|
|
|
46
60
|
provider: string | null;
|
|
47
61
|
model: string | null;
|
|
48
62
|
apiKeyEnv: string | null;
|
|
49
|
-
/** True if the env var name is non-empty and the env var is set. */
|
|
50
63
|
apiKeyPresent: boolean;
|
|
51
64
|
classification: DoctorClassification;
|
|
52
65
|
reason: string;
|
|
53
66
|
nextAction: string;
|
|
54
|
-
/** Source of the discovered config (e.g., 'workflows.yaml', 'cli_flag', 'default'). */
|
|
55
67
|
source: string;
|
|
56
68
|
}
|
|
57
69
|
|
|
@@ -63,14 +75,27 @@ export interface FeatureFlagSummary {
|
|
|
63
75
|
warnings: string[];
|
|
64
76
|
}
|
|
65
77
|
|
|
78
|
+
export interface InternalAgentDiagnostics {
|
|
79
|
+
name: string;
|
|
80
|
+
enabled: boolean;
|
|
81
|
+
runtimeProfileId: string;
|
|
82
|
+
runtimeProfileLabel: string;
|
|
83
|
+
readiness: 'ready' | 'not_ready' | 'needs_setup' | 'disabled' | 'unknown';
|
|
84
|
+
provider: string | null;
|
|
85
|
+
model: string | null;
|
|
86
|
+
apiKeyEnv: string | null;
|
|
87
|
+
apiKeyPresent: boolean;
|
|
88
|
+
reason: string;
|
|
89
|
+
nextAction: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
66
92
|
export interface DoctorOutput {
|
|
67
93
|
status: DoctorStatus;
|
|
68
94
|
workspaceDir: string;
|
|
69
95
|
pdConfigPaths: {
|
|
70
96
|
workspaceDir: ConfigPathEntry;
|
|
71
97
|
pdDir: ConfigPathEntry;
|
|
72
|
-
|
|
73
|
-
workflowsYaml: ConfigPathEntry;
|
|
98
|
+
configYaml: ConfigPathEntry;
|
|
74
99
|
stateDb: ConfigPathEntry;
|
|
75
100
|
};
|
|
76
101
|
openclawConfigPaths: {
|
|
@@ -78,36 +103,17 @@ export interface DoctorOutput {
|
|
|
78
103
|
openclawConfig: ConfigPathEntry;
|
|
79
104
|
};
|
|
80
105
|
featureFlags: FeatureFlagSummary;
|
|
106
|
+
internalAgents: InternalAgentDiagnostics[];
|
|
81
107
|
providerHealth: ProviderHealthEntry[];
|
|
82
|
-
internalAgents: {
|
|
83
|
-
correctionObserver: {
|
|
84
|
-
enabled: boolean;
|
|
85
|
-
flagSource: string;
|
|
86
|
-
status: 'disabled' | 'configured' | 'auth_missing' | 'config_missing' | 'unavailable';
|
|
87
|
-
configSource: 'workflows.yaml' | 'env' | 'missing' | 'unavailable';
|
|
88
|
-
provider: string | null;
|
|
89
|
-
model: string | null;
|
|
90
|
-
apiKeyEnv: string | null;
|
|
91
|
-
apiKeyPresent: boolean;
|
|
92
|
-
reason: string;
|
|
93
|
-
nextAction: string;
|
|
94
|
-
};
|
|
95
|
-
};
|
|
96
108
|
warnings: string[];
|
|
97
109
|
reason?: string;
|
|
98
110
|
nextActions: string[];
|
|
111
|
+
/** Legacy files detected (informational only, not used for resolution) */
|
|
112
|
+
legacyFilesDetected: string[];
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
102
116
|
|
|
103
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
104
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Resolve the OpenClaw home directory used to look up `openclaw.json` and
|
|
109
|
-
* extension state. PD does not own this path — it only reports its existence.
|
|
110
|
-
*/
|
|
111
117
|
export function getOpenClawHome(): string {
|
|
112
118
|
const home = process.env.HOME
|
|
113
119
|
|| process.env.USERPROFILE
|
|
@@ -120,9 +126,6 @@ export function getOpenClawConfigPath(): string {
|
|
|
120
126
|
return path.join(getOpenClawHome(), 'openclaw.json');
|
|
121
127
|
}
|
|
122
128
|
|
|
123
|
-
/**
|
|
124
|
-
* Build a `ConfigPathEntry` with existence check.
|
|
125
|
-
*/
|
|
126
129
|
function pathEntry(p: string): ConfigPathEntry {
|
|
127
130
|
return { path: p, exists: fs.existsSync(p) };
|
|
128
131
|
}
|
|
@@ -145,10 +148,10 @@ function classifyText(text: string): DoctorClassification | null {
|
|
|
145
148
|
return null;
|
|
146
149
|
}
|
|
147
150
|
|
|
151
|
+
// ─── State DB Inspection (unchanged from PRI-299) ────────────────────────────
|
|
152
|
+
|
|
148
153
|
export interface InspectStateDbOptions {
|
|
149
|
-
/** Max rows to scan from tasks table. */
|
|
150
154
|
maxRows?: number;
|
|
151
|
-
/** Max age in ms — only signals within this window are considered "recent". */
|
|
152
155
|
maxAgeMs?: number;
|
|
153
156
|
}
|
|
154
157
|
|
|
@@ -160,23 +163,13 @@ export interface RecentProviderSignal {
|
|
|
160
163
|
|
|
161
164
|
export interface StateDbSignalResult {
|
|
162
165
|
signal: RecentProviderSignal | null;
|
|
163
|
-
/** True if the DB was found and could be opened in readonly mode. */
|
|
164
166
|
dbReachable: boolean;
|
|
165
|
-
/** Optional warning when the DB exists but couldn't be read. */
|
|
166
167
|
warning?: string;
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
const DEFAULT_MAX_ROWS = 50;
|
|
170
|
-
const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
171
|
+
const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
171
172
|
|
|
172
|
-
/**
|
|
173
|
-
* Read the state.db recent provider error signal (best-effort, structural only).
|
|
174
|
-
*
|
|
175
|
-
* We inspect recent task/run rows in `<workspaceDir>/.pd/state.db` to detect
|
|
176
|
-
* `rate_limit` / `auth_missing` / `unavailable` signals from the live pipeline.
|
|
177
|
-
* This is a bounded best-effort probe — if the DB is missing or unreadable we
|
|
178
|
-
* return `null` and let the doctor fall back to config-only classification.
|
|
179
|
-
*/
|
|
180
173
|
export async function inspectStateDbForProviderSignal(
|
|
181
174
|
stateDbPath: string,
|
|
182
175
|
nowMs: number = Date.now(),
|
|
@@ -201,13 +194,11 @@ export async function inspectStateDbForProviderSignal(
|
|
|
201
194
|
}
|
|
202
195
|
|
|
203
196
|
try {
|
|
204
|
-
// Check for tasks table — drift-safe (ERR-026): assert via pragma, not assumption.
|
|
205
197
|
const tableInfo = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='tasks'").get() as { name: string } | undefined;
|
|
206
198
|
if (!tableInfo) {
|
|
207
199
|
return { signal: null, dbReachable: true, warning: 'state.db has no tasks table — pipeline not initialized yet' };
|
|
208
200
|
}
|
|
209
201
|
|
|
210
|
-
// Find columns defensively.
|
|
211
202
|
const taskCols = db.prepare("PRAGMA table_info(tasks)").all() as { name: string }[];
|
|
212
203
|
const taskColNames = new Set(taskCols.map((c) => c.name));
|
|
213
204
|
const hasErrorMessage = taskColNames.has('error_message');
|
|
@@ -222,7 +213,6 @@ export async function inspectStateDbForProviderSignal(
|
|
|
222
213
|
return { signal: null, dbReachable: true, warning: 'state.db tasks table has no error_message column — provider signal unavailable' };
|
|
223
214
|
}
|
|
224
215
|
|
|
225
|
-
// Build a bounded scan: read up to maxRows recent failed tasks.
|
|
226
216
|
const errorCol = hasErrorMessage ? 'error_message' : 'last_error_message';
|
|
227
217
|
const tsCol = hasUpdatedAt ? 'updated_at' : hasLastErrorAt ? 'last_error_at' : hasCreatedAt ? 'created_at' : null;
|
|
228
218
|
const tsExpr = tsCol ? `, ${tsCol} AS ts` : '';
|
|
@@ -243,7 +233,6 @@ export async function inspectStateDbForProviderSignal(
|
|
|
243
233
|
}
|
|
244
234
|
}
|
|
245
235
|
|
|
246
|
-
// Also check for explicit error_category/failure_category columns if present.
|
|
247
236
|
if (hasErrorCategory || hasFailureCategory) {
|
|
248
237
|
const catCol = hasErrorCategory ? 'error_category' : 'failure_category';
|
|
249
238
|
const catRows = db.prepare(
|
|
@@ -280,144 +269,127 @@ export async function inspectStateDbForProviderSignal(
|
|
|
280
269
|
}
|
|
281
270
|
}
|
|
282
271
|
|
|
283
|
-
|
|
284
|
-
* Resolve the provider/model/apiKeyEnv from the workspace's `workflows.yaml`
|
|
285
|
-
* funnel policy. Returns `null` for each field when not configured.
|
|
286
|
-
*
|
|
287
|
-
* NEVER leaks api key values or env var values.
|
|
288
|
-
*/
|
|
289
|
-
export interface ProviderConfigFromWorkflows {
|
|
290
|
-
provider: string | null;
|
|
291
|
-
model: string | null;
|
|
292
|
-
apiKeyEnv: string | null;
|
|
293
|
-
baseUrl: string | null;
|
|
294
|
-
source: 'workflows.yaml' | 'cli_flag' | 'default' | 'missing';
|
|
295
|
-
/** True if the workflows.yaml file was found and parseable. */
|
|
296
|
-
workflowsFound: boolean;
|
|
297
|
-
/** Parse warning, if any. */
|
|
298
|
-
parseWarning?: string;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const DIAGNOSTIC_FUNNEL_ID = 'pd-runtime-v2-diagnosis';
|
|
272
|
+
// ─── Internal Agent Diagnostics from .pd/config.yaml ─────────────────────────
|
|
302
273
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
):
|
|
307
|
-
|
|
308
|
-
if (!fs.existsSync(workflowsPath)) {
|
|
309
|
-
// Fall back to CLI flag values if provided.
|
|
310
|
-
if (opts.cliProvider || opts.cliModel || opts.cliApiKeyEnv) {
|
|
311
|
-
return {
|
|
312
|
-
provider: opts.cliProvider ?? null,
|
|
313
|
-
model: opts.cliModel ?? null,
|
|
314
|
-
apiKeyEnv: opts.cliApiKeyEnv ?? null,
|
|
315
|
-
baseUrl: opts.cliBaseUrl ?? null,
|
|
316
|
-
source: 'cli_flag',
|
|
317
|
-
workflowsFound: false,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
274
|
+
function diagnoseInternalAgent(
|
|
275
|
+
agent: RedactedAgentSummary,
|
|
276
|
+
profile: RedactedRuntimeProfileSummary | undefined,
|
|
277
|
+
): InternalAgentDiagnostics {
|
|
278
|
+
if (!agent.enabled) {
|
|
320
279
|
return {
|
|
280
|
+
name: agent.name,
|
|
281
|
+
enabled: false,
|
|
282
|
+
runtimeProfileId: agent.runtimeProfileId,
|
|
283
|
+
runtimeProfileLabel: agent.runtimeProfileLabel,
|
|
284
|
+
readiness: 'disabled',
|
|
321
285
|
provider: null,
|
|
322
286
|
model: null,
|
|
323
287
|
apiKeyEnv: null,
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
288
|
+
apiKeyPresent: false,
|
|
289
|
+
reason: `${agent.name} is disabled in .pd/config.yaml`,
|
|
290
|
+
nextAction: `Set internalAgents.agents.${agent.name}.enabled=true in .pd/config.yaml to enable`,
|
|
327
291
|
};
|
|
328
292
|
}
|
|
329
293
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
raw = fs.readFileSync(workflowsPath, 'utf8');
|
|
333
|
-
} catch (err) {
|
|
294
|
+
// Agent is enabled — check profile readiness
|
|
295
|
+
if (!profile) {
|
|
334
296
|
return {
|
|
297
|
+
name: agent.name,
|
|
298
|
+
enabled: true,
|
|
299
|
+
runtimeProfileId: agent.runtimeProfileId,
|
|
300
|
+
runtimeProfileLabel: agent.runtimeProfileLabel,
|
|
301
|
+
readiness: 'needs_setup',
|
|
335
302
|
provider: null,
|
|
336
303
|
model: null,
|
|
337
304
|
apiKeyEnv: null,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
parseWarning: `workflows.yaml read failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
305
|
+
apiKeyPresent: false,
|
|
306
|
+
reason: `Runtime profile '${agent.runtimeProfileId}' not found in .pd/config.yaml`,
|
|
307
|
+
nextAction: `Add runtime profile '${agent.runtimeProfileId}' to .pd/config.yaml runtimeProfiles, or change the agent's runtimeProfile reference`,
|
|
342
308
|
};
|
|
343
309
|
}
|
|
344
310
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
return {
|
|
350
|
-
provider: null,
|
|
351
|
-
model: null,
|
|
352
|
-
apiKeyEnv: null,
|
|
353
|
-
baseUrl: null,
|
|
354
|
-
source: 'workflows.yaml',
|
|
355
|
-
workflowsFound: true,
|
|
356
|
-
parseWarning: `workflows.yaml parse error: ${err instanceof Error ? err.message : String(err)}`,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
311
|
+
// Check apiKeyEnv for pi-ai profiles
|
|
312
|
+
if (profile.type === 'pi-ai') {
|
|
313
|
+
const apiKeyEnv = profile.apiKeyEnv ?? null;
|
|
314
|
+
const apiKeyPresent = !!apiKeyEnv && Object.prototype.hasOwnProperty.call(process.env, apiKeyEnv) && !!process.env[apiKeyEnv];
|
|
359
315
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
316
|
+
if (!apiKeyEnv) {
|
|
317
|
+
return {
|
|
318
|
+
name: agent.name,
|
|
319
|
+
enabled: true,
|
|
320
|
+
runtimeProfileId: agent.runtimeProfileId,
|
|
321
|
+
runtimeProfileLabel: agent.runtimeProfileLabel,
|
|
322
|
+
readiness: 'needs_setup',
|
|
323
|
+
provider: null,
|
|
324
|
+
model: null,
|
|
325
|
+
apiKeyEnv: null,
|
|
326
|
+
apiKeyPresent: false,
|
|
327
|
+
reason: `pi-ai profile '${profile.id}' missing apiKeyEnv`,
|
|
328
|
+
nextAction: `Add apiKeyEnv to runtime profile '${profile.id}' in .pd/config.yaml`,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
371
331
|
|
|
372
|
-
|
|
373
|
-
|
|
332
|
+
if (!apiKeyPresent) {
|
|
333
|
+
return {
|
|
334
|
+
name: agent.name,
|
|
335
|
+
enabled: true,
|
|
336
|
+
runtimeProfileId: agent.runtimeProfileId,
|
|
337
|
+
runtimeProfileLabel: agent.runtimeProfileLabel,
|
|
338
|
+
readiness: 'needs_setup',
|
|
339
|
+
provider: null,
|
|
340
|
+
model: null,
|
|
341
|
+
apiKeyEnv,
|
|
342
|
+
apiKeyPresent: false,
|
|
343
|
+
reason: `Environment variable '${apiKeyEnv}' is not set or empty`,
|
|
344
|
+
nextAction: `Set the environment variable '${apiKeyEnv}' with a valid API key`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// pi-ai with key present — check state.db for signals
|
|
374
349
|
return {
|
|
350
|
+
name: agent.name,
|
|
351
|
+
enabled: true,
|
|
352
|
+
runtimeProfileId: agent.runtimeProfileId,
|
|
353
|
+
runtimeProfileLabel: agent.runtimeProfileLabel,
|
|
354
|
+
readiness: 'not_ready', // runtime availability unknown without actual probe
|
|
375
355
|
provider: null,
|
|
376
356
|
model: null,
|
|
377
|
-
apiKeyEnv
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
parseWarning: 'workflows.yaml: funnels is not an array',
|
|
357
|
+
apiKeyEnv,
|
|
358
|
+
apiKeyPresent: true,
|
|
359
|
+
reason: `pi-ai profile configured with apiKeyEnv='${apiKeyEnv}' (key present); runtime availability unknown`,
|
|
360
|
+
nextAction: 'Run pd runtime probe to verify end-to-end connectivity',
|
|
382
361
|
};
|
|
383
362
|
}
|
|
384
363
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (!isRecord(f)) continue;
|
|
388
|
-
if (f.workflowId === DIAGNOSTIC_FUNNEL_ID && isRecord(f.policy)) {
|
|
389
|
-
const { policy: candidate } = f;
|
|
390
|
-
if (isRecord(candidate)) {
|
|
391
|
-
policy = candidate;
|
|
392
|
-
break;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (policy === null) {
|
|
364
|
+
// OpenClaw profile
|
|
365
|
+
if (profile.readiness === 'needs_setup') {
|
|
398
366
|
return {
|
|
367
|
+
name: agent.name,
|
|
368
|
+
enabled: true,
|
|
369
|
+
runtimeProfileId: agent.runtimeProfileId,
|
|
370
|
+
runtimeProfileLabel: agent.runtimeProfileLabel,
|
|
371
|
+
readiness: 'needs_setup',
|
|
399
372
|
provider: null,
|
|
400
373
|
model: null,
|
|
401
374
|
apiKeyEnv: null,
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
parseWarning: `workflows.yaml: funnel '${DIAGNOSTIC_FUNNEL_ID}' not found`,
|
|
375
|
+
apiKeyPresent: false,
|
|
376
|
+
reason: `OpenClaw profile '${profile.id}' needs setup (missing provider/model)`,
|
|
377
|
+
nextAction: `Configure provider and model in runtime profile '${profile.id}' in .pd/config.yaml`,
|
|
406
378
|
};
|
|
407
379
|
}
|
|
408
380
|
|
|
409
|
-
const provider = typeof policy.provider === 'string' ? policy.provider : null;
|
|
410
|
-
const model = typeof policy.model === 'string' ? policy.model : null;
|
|
411
|
-
const apiKeyEnv = typeof policy.apiKeyEnv === 'string' ? policy.apiKeyEnv : null;
|
|
412
|
-
const baseUrl = typeof policy.baseUrl === 'string' ? policy.baseUrl : null;
|
|
413
|
-
|
|
414
381
|
return {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
382
|
+
name: agent.name,
|
|
383
|
+
enabled: true,
|
|
384
|
+
runtimeProfileId: agent.runtimeProfileId,
|
|
385
|
+
runtimeProfileLabel: agent.runtimeProfileLabel,
|
|
386
|
+
readiness: 'ready',
|
|
387
|
+
provider: null,
|
|
388
|
+
model: null,
|
|
389
|
+
apiKeyEnv: null,
|
|
390
|
+
apiKeyPresent: false,
|
|
391
|
+
reason: `OpenClaw profile '${profile.id}' is configured and ready`,
|
|
392
|
+
nextAction: 'No action required',
|
|
421
393
|
};
|
|
422
394
|
}
|
|
423
395
|
|
|
@@ -425,291 +397,149 @@ export async function resolveProviderConfigFromWorkflows(
|
|
|
425
397
|
|
|
426
398
|
export interface BuildDoctorInput {
|
|
427
399
|
workspaceDir: string;
|
|
428
|
-
/** Optional CLI overrides for provider/model/apiKeyEnv. */
|
|
429
|
-
cliProvider?: string;
|
|
430
|
-
cliModel?: string;
|
|
431
|
-
cliApiKeyEnv?: string;
|
|
432
|
-
cliBaseUrl?: string;
|
|
433
400
|
}
|
|
434
401
|
|
|
435
|
-
const MVP_CHANNELS = new Set(['prompt', 'code_tool_hook', 'defer_archive']);
|
|
436
|
-
|
|
437
402
|
export async function buildDoctorOutput(input: BuildDoctorInput): Promise<DoctorOutput> {
|
|
438
403
|
const workspaceDir = path.resolve(input.workspaceDir);
|
|
439
|
-
const pdDir = path.join(workspaceDir,
|
|
440
|
-
const
|
|
441
|
-
const workflowsPath = path.join(workspaceDir, '.state', 'workflows.yaml');
|
|
404
|
+
const pdDir = path.join(workspaceDir, '.pd');
|
|
405
|
+
const configYamlPath = getPdConfigPath(workspaceDir);
|
|
442
406
|
const stateDbPath = path.join(pdDir, 'state.db');
|
|
443
407
|
|
|
444
|
-
// 1)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
408
|
+
// 1) Load PD config from .pd/config.yaml
|
|
409
|
+
const loadResult = loadPdConfig(workspaceDir);
|
|
410
|
+
const flags = computeFlagsFromLoadResult(loadResult);
|
|
411
|
+
const redacted = redactLoadResult(loadResult);
|
|
412
|
+
|
|
413
|
+
// 2) Feature flag summary
|
|
414
|
+
const enabledMvpChannels: string[] = [];
|
|
415
|
+
const disabledFlags: string[] = [];
|
|
416
|
+
for (const flag of Object.values(flags.flags)) {
|
|
417
|
+
if (flag.enabled && MVP_CHANNEL_IDS.includes(flag.id as typeof MVP_CHANNEL_IDS[number])) {
|
|
418
|
+
enabledMvpChannels.push(flag.id);
|
|
419
|
+
} else if (!flag.enabled) {
|
|
420
|
+
disabledFlags.push(flag.id);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
451
423
|
|
|
452
|
-
|
|
453
|
-
|
|
424
|
+
const featureFlags: FeatureFlagSummary = {
|
|
425
|
+
source: loadResult.ok ? loadResult.source : 'malformed',
|
|
426
|
+
configPath: loadResult.configPath,
|
|
427
|
+
enabledMvpChannels,
|
|
428
|
+
disabledFlags,
|
|
429
|
+
warnings: flags.warnings,
|
|
430
|
+
};
|
|
454
431
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
for (const flag of Object.values(flags.flags)) {
|
|
460
|
-
if (flag.enabled && MVP_CHANNELS.has(flag.id)) {
|
|
461
|
-
enabledMvpChannels.push(flag.id);
|
|
462
|
-
} else if (!flag.enabled) {
|
|
463
|
-
disabledFlags.push(flag.id);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (flags.flags && flags.flags.correction_observer) {
|
|
467
|
-
correctionObserverEnabled = flags.flags.correction_observer.enabled;
|
|
468
|
-
coFlagSource = flags.source;
|
|
469
|
-
}
|
|
470
|
-
} catch (err) {
|
|
471
|
-
hasFeatureFlagsError = true;
|
|
472
|
-
featureFlagsErrorMessage = err instanceof Error ? err.message : String(err);
|
|
473
|
-
flagSource = 'unavailable';
|
|
474
|
-
flagWarnings = [`feature flags unavailable: ${featureFlagsErrorMessage}`];
|
|
475
|
-
coFlagSource = 'unavailable';
|
|
432
|
+
// 3) Internal agent diagnostics from .pd/config.yaml
|
|
433
|
+
const profileMap = new Map<string, RedactedRuntimeProfileSummary>();
|
|
434
|
+
for (const p of redacted.runtimeProfiles) {
|
|
435
|
+
profileMap.set(p.id, p);
|
|
476
436
|
}
|
|
477
437
|
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
cliModel: input.cliModel,
|
|
484
|
-
cliApiKeyEnv: input.cliApiKeyEnv,
|
|
485
|
-
cliBaseUrl: input.cliBaseUrl,
|
|
486
|
-
},
|
|
487
|
-
);
|
|
438
|
+
const internalAgents: InternalAgentDiagnostics[] = [];
|
|
439
|
+
for (const agent of redacted.agents) {
|
|
440
|
+
const profile = profileMap.get(agent.runtimeProfileId);
|
|
441
|
+
internalAgents.push(diagnoseInternalAgent(agent, profile));
|
|
442
|
+
}
|
|
488
443
|
|
|
489
|
-
//
|
|
444
|
+
// 4) Provider health — derived from internal agents that are enabled
|
|
490
445
|
const providerHealth: ProviderHealthEntry[] = [];
|
|
491
446
|
const warnings: string[] = [];
|
|
492
447
|
const nextActions: string[] = [];
|
|
493
448
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
reason: 'No provider configured (workflows.yaml funnel policy missing and no CLI override)',
|
|
502
|
-
nextAction: 'Configure provider/model/apiKeyEnv in workflows.yaml pd-runtime-v2-diagnosis policy, or pass --provider/--model/--apiKeyEnv flags',
|
|
503
|
-
source: providerCfg.source,
|
|
504
|
-
});
|
|
505
|
-
warnings.push('No provider configured for the diagnostic funnel');
|
|
506
|
-
nextActions.push('Configure provider/model/apiKeyEnv in workflows.yaml or pass CLI flags');
|
|
507
|
-
} else {
|
|
508
|
-
const apiKeyEnvName = providerCfg.apiKeyEnv;
|
|
509
|
-
const apiKeyPresent = !!apiKeyEnvName && Object.prototype.hasOwnProperty.call(process.env, apiKeyEnvName) && !!process.env[apiKeyEnvName];
|
|
510
|
-
|
|
511
|
-
let classification: DoctorClassification;
|
|
512
|
-
let reason: string;
|
|
513
|
-
let nextAction: string;
|
|
514
|
-
|
|
515
|
-
if (!providerCfg.provider || !providerCfg.model) {
|
|
516
|
-
classification = 'config_missing';
|
|
517
|
-
reason = !providerCfg.provider
|
|
518
|
-
? 'Provider not configured'
|
|
519
|
-
: 'Model not configured';
|
|
520
|
-
nextAction = 'Set provider and model in workflows.yaml funnel policy or pass --provider/--model flags';
|
|
521
|
-
} else if (!apiKeyEnvName) {
|
|
522
|
-
classification = 'auth_missing';
|
|
523
|
-
reason = 'apiKeyEnv is not set in workflows.yaml';
|
|
524
|
-
nextAction = "Add 'apiKeyEnv: <ENV_VAR_NAME>' to workflows.yaml funnel policy and ensure the env var holds a valid key";
|
|
525
|
-
} else if (!apiKeyPresent) {
|
|
526
|
-
classification = 'auth_missing';
|
|
527
|
-
reason = `Environment variable '${apiKeyEnvName}' is not set or empty`;
|
|
528
|
-
nextAction = `Set the environment variable '${apiKeyEnvName}' with a valid API key, then retry`;
|
|
529
|
-
} else {
|
|
530
|
-
// Config is well-formed and the key is present — check state.db for rate-limit / unavailable signals.
|
|
531
|
-
const signalResult = await inspectStateDbForProviderSignal(stateDbPath);
|
|
532
|
-
if (signalResult.warning) {
|
|
533
|
-
warnings.push(signalResult.warning);
|
|
534
|
-
}
|
|
535
|
-
if (signalResult.signal) {
|
|
536
|
-
const { classification: cls, reason: rsn } = signalResult.signal;
|
|
537
|
-
classification = cls;
|
|
538
|
-
reason = rsn;
|
|
539
|
-
switch (classification) {
|
|
540
|
-
case 'rate_limit':
|
|
541
|
-
nextAction = `Wait for the provider's rate limit to reset, or reduce request rate. Recent error in state.db.`;
|
|
542
|
-
break;
|
|
543
|
-
case 'auth_missing':
|
|
544
|
-
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}'.`;
|
|
545
|
-
break;
|
|
546
|
-
case 'unavailable':
|
|
547
|
-
nextAction = `Provider/model temporarily unavailable. Try a different model or retry later. Check provider status page.`;
|
|
548
|
-
break;
|
|
549
|
-
default:
|
|
550
|
-
nextAction = 'Inspect recent task errors in state.db for details.';
|
|
551
|
-
}
|
|
552
|
-
} else {
|
|
553
|
-
classification = 'healthy';
|
|
554
|
-
reason = 'Provider, model, and apiKeyEnv are configured; env var is set; no recent error signals';
|
|
555
|
-
nextAction = 'Run pd diagnose run to validate end-to-end provider connectivity';
|
|
556
|
-
}
|
|
449
|
+
// Add config-level warnings
|
|
450
|
+
warnings.push(...loadResult.warnings);
|
|
451
|
+
warnings.push(...flags.warnings);
|
|
452
|
+
|
|
453
|
+
if (!loadResult.ok) {
|
|
454
|
+
for (const err of loadResult.errors) {
|
|
455
|
+
warnings.push(`Config error at ${err.path}: ${err.reason}`);
|
|
557
456
|
}
|
|
457
|
+
nextActions.push(loadResult.errors[0]?.nextAction ?? 'Fix .pd/config.yaml and retry');
|
|
458
|
+
}
|
|
558
459
|
|
|
460
|
+
// Check for enabled agents that need setup
|
|
461
|
+
const needsSetupAgents = internalAgents.filter(a => a.enabled && (a.readiness === 'needs_setup' || a.readiness === 'not_ready'));
|
|
462
|
+
for (const agent of needsSetupAgents) {
|
|
463
|
+
// not_ready = key present but not probed → needs_probe (degraded), NOT auth_missing (failed)
|
|
464
|
+
const classification: DoctorClassification = agent.readiness === 'needs_setup'
|
|
465
|
+
? 'config_missing'
|
|
466
|
+
: 'needs_probe';
|
|
559
467
|
providerHealth.push({
|
|
560
|
-
provider:
|
|
561
|
-
model:
|
|
562
|
-
apiKeyEnv:
|
|
563
|
-
apiKeyPresent,
|
|
468
|
+
provider: null,
|
|
469
|
+
model: null,
|
|
470
|
+
apiKeyEnv: agent.apiKeyEnv,
|
|
471
|
+
apiKeyPresent: agent.apiKeyPresent,
|
|
564
472
|
classification,
|
|
565
|
-
reason,
|
|
566
|
-
nextAction,
|
|
567
|
-
source:
|
|
473
|
+
reason: agent.reason,
|
|
474
|
+
nextAction: agent.nextAction,
|
|
475
|
+
source: 'config.yaml',
|
|
568
476
|
});
|
|
569
477
|
}
|
|
570
478
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// 4) Aggregate feature-flag warnings
|
|
581
|
-
for (const w of flagWarnings) {
|
|
582
|
-
warnings.push(w);
|
|
583
|
-
}
|
|
584
|
-
if (flagWarnings.length > 0 && !hasFeatureFlagsError) {
|
|
585
|
-
nextActions.push('Review .pd/feature-flags.yaml — see warnings for details');
|
|
586
|
-
}
|
|
587
|
-
if (hasFeatureFlagsError) {
|
|
588
|
-
warnings.push(`feature flags unavailable: ${featureFlagsErrorMessage}`);
|
|
589
|
-
nextActions.push(`Check that ${featureFlagsPath} is a readable file, not a directory.`);
|
|
590
|
-
nextActions.push('Re-run npx create-principles-disciple if the config was generated incorrectly.');
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// 4.5) CorrectionObserver Diagnostics
|
|
594
|
-
let coStatus: 'disabled' | 'configured' | 'auth_missing' | 'config_missing' | 'unavailable';
|
|
595
|
-
let coConfigSource: 'workflows.yaml' | 'env' | 'missing' | 'unavailable';
|
|
596
|
-
let coProvider: string | null = null;
|
|
597
|
-
let coModel: string | null = null;
|
|
598
|
-
let coApiKeyEnv: string | null = null;
|
|
599
|
-
let coApiKeyPresent = false;
|
|
600
|
-
let coReason: string;
|
|
601
|
-
let coNextAction: string;
|
|
602
|
-
|
|
603
|
-
if (!correctionObserverEnabled) {
|
|
604
|
-
coStatus = 'disabled';
|
|
605
|
-
coConfigSource = 'missing';
|
|
606
|
-
coProvider = null;
|
|
607
|
-
coModel = null;
|
|
608
|
-
coApiKeyEnv = null;
|
|
609
|
-
coApiKeyPresent = false;
|
|
610
|
-
coReason = 'CorrectionObserver is disabled via feature flags';
|
|
611
|
-
coNextAction = 'Set correction_observer.enabled=true in .pd/feature-flags.yaml to enable it';
|
|
612
|
-
} else {
|
|
613
|
-
const coWorkflowsPath = path.join(workspaceDir, '.state', 'workflows.yaml');
|
|
614
|
-
let coWorkflowsFound = false;
|
|
615
|
-
let coWorkflowsParseError = false;
|
|
616
|
-
let coWorkflowsParseErrorMessage = '';
|
|
617
|
-
let coWorkflowPolicy: Record<string, unknown> | null = null;
|
|
618
|
-
|
|
619
|
-
if (fs.existsSync(coWorkflowsPath)) {
|
|
620
|
-
coWorkflowsFound = true;
|
|
621
|
-
try {
|
|
622
|
-
const raw = fs.readFileSync(coWorkflowsPath, 'utf8');
|
|
623
|
-
const parsed = yaml.load(raw);
|
|
624
|
-
if (isRecord(parsed)) {
|
|
625
|
-
const funnelsRaw = parsed.funnels;
|
|
626
|
-
if (Array.isArray(funnelsRaw)) {
|
|
627
|
-
for (const f of funnelsRaw) {
|
|
628
|
-
if (isRecord(f) && f.workflowId === 'pd-correction-observer' && isRecord(f.policy)) {
|
|
629
|
-
coWorkflowPolicy = f.policy;
|
|
630
|
-
break;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
} catch (err) {
|
|
636
|
-
coWorkflowsParseError = true;
|
|
637
|
-
coWorkflowsParseErrorMessage = err instanceof Error ? err.message : String(err);
|
|
638
|
-
}
|
|
479
|
+
// Check state.db for rate-limit / unavailable signals for enabled agents
|
|
480
|
+
const readyAgents = internalAgents.filter(a => a.enabled && a.readiness === 'ready');
|
|
481
|
+
if (readyAgents.length > 0) {
|
|
482
|
+
const signalResult = await inspectStateDbForProviderSignal(stateDbPath);
|
|
483
|
+
if (signalResult.warning) {
|
|
484
|
+
warnings.push(signalResult.warning);
|
|
639
485
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
coStatus = 'config_missing';
|
|
655
|
-
coReason = !coProvider ? 'Provider not configured in workflows.yaml policy' : 'Model not configured in workflows.yaml policy';
|
|
656
|
-
coNextAction = 'Set provider and model in pd-correction-observer policy in workflows.yaml';
|
|
657
|
-
} else if (!coApiKeyEnv) {
|
|
658
|
-
coStatus = 'auth_missing';
|
|
659
|
-
coReason = 'apiKeyEnv is not set in workflows.yaml pd-correction-observer policy';
|
|
660
|
-
coNextAction = "Add 'apiKeyEnv: <ENV_VAR_NAME>' to workflows.yaml pd-correction-observer policy and ensure the env var holds a valid key";
|
|
661
|
-
} else if (!coApiKeyPresent) {
|
|
662
|
-
coStatus = 'auth_missing';
|
|
663
|
-
coReason = `Environment variable '${coApiKeyEnv}' is not set or empty`;
|
|
664
|
-
coNextAction = `Set the environment variable '${coApiKeyEnv}' with a valid API key, or disable correction_observer in feature flags`;
|
|
665
|
-
} else {
|
|
666
|
-
coStatus = 'configured';
|
|
667
|
-
coReason = 'CorrectionObserver is configured and ready via workflows.yaml';
|
|
668
|
-
coNextAction = 'No action required; correction observer is active';
|
|
669
|
-
}
|
|
486
|
+
if (signalResult.signal) {
|
|
487
|
+
const { classification: cls, reason: rsn } = signalResult.signal;
|
|
488
|
+
providerHealth.push({
|
|
489
|
+
provider: null,
|
|
490
|
+
model: null,
|
|
491
|
+
apiKeyEnv: null,
|
|
492
|
+
apiKeyPresent: false,
|
|
493
|
+
classification: cls,
|
|
494
|
+
reason: rsn,
|
|
495
|
+
nextAction: cls === 'rate_limit'
|
|
496
|
+
? "Wait for the provider's rate limit to reset, or reduce request rate"
|
|
497
|
+
: 'Provider/model temporarily unavailable. Try a different model or retry later.',
|
|
498
|
+
source: 'state.db',
|
|
499
|
+
});
|
|
670
500
|
} else {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
} else {
|
|
682
|
-
coStatus = 'configured';
|
|
683
|
-
coReason = 'CorrectionObserver is configured and ready via env overrides/defaults';
|
|
684
|
-
coNextAction = 'No action required; correction observer is active';
|
|
685
|
-
}
|
|
501
|
+
providerHealth.push({
|
|
502
|
+
provider: null,
|
|
503
|
+
model: null,
|
|
504
|
+
apiKeyEnv: null,
|
|
505
|
+
apiKeyPresent: false,
|
|
506
|
+
classification: 'healthy',
|
|
507
|
+
reason: 'Config is valid; no recent error signals in state.db',
|
|
508
|
+
nextAction: 'Run pd runtime probe to verify end-to-end connectivity',
|
|
509
|
+
source: 'config.yaml',
|
|
510
|
+
});
|
|
686
511
|
}
|
|
687
512
|
}
|
|
688
513
|
|
|
689
514
|
// 5) Compute overall status
|
|
690
515
|
let status: DoctorStatus = 'ok';
|
|
691
516
|
const classifications = providerHealth.map((p) => p.classification);
|
|
692
|
-
if (classifications.includes('rate_limit') || classifications.includes('unavailable')) {
|
|
517
|
+
if (classifications.includes('rate_limit') || classifications.includes('unavailable') || classifications.includes('needs_probe')) {
|
|
693
518
|
status = 'degraded';
|
|
694
519
|
}
|
|
695
520
|
if (classifications.includes('auth_missing') || classifications.includes('config_missing')) {
|
|
696
521
|
status = 'failed';
|
|
697
522
|
}
|
|
698
|
-
if (
|
|
523
|
+
if (!loadResult.ok) {
|
|
524
|
+
status = 'failed';
|
|
525
|
+
}
|
|
526
|
+
if ((warnings.length > 0) && status === 'ok') {
|
|
699
527
|
status = 'degraded';
|
|
700
528
|
}
|
|
701
529
|
|
|
702
530
|
// 6) Reason
|
|
703
531
|
let reason: string | undefined;
|
|
704
532
|
if (status === 'failed') {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
reason = `Provider auth/config missing: ${failed.map((p) => p.classification).join(', ')}`;
|
|
533
|
+
if (!loadResult.ok) {
|
|
534
|
+
reason = `Config validation failed: ${loadResult.errors.map(e => e.reason).join('; ')}`;
|
|
708
535
|
} else {
|
|
709
|
-
|
|
536
|
+
const failed = providerHealth.filter((p) => p.classification === 'auth_missing' || p.classification === 'config_missing');
|
|
537
|
+
reason = failed.length > 0
|
|
538
|
+
? `Provider auth/config missing: ${failed.map((p) => p.classification).join(', ')}`
|
|
539
|
+
: 'Configuration is missing required fields';
|
|
710
540
|
}
|
|
711
541
|
} else if (status === 'degraded') {
|
|
712
|
-
const degraded = providerHealth.filter((p) => p.classification === 'rate_limit' || p.classification === 'unavailable');
|
|
542
|
+
const degraded = providerHealth.filter((p) => p.classification === 'rate_limit' || p.classification === 'unavailable' || p.classification === 'needs_probe');
|
|
713
543
|
if (degraded.length > 0) {
|
|
714
544
|
reason = `Provider connectivity degraded: ${degraded.map((p) => p.classification).join(', ')}`;
|
|
715
545
|
} else if (warnings.length > 0) {
|
|
@@ -717,45 +547,26 @@ export async function buildDoctorOutput(input: BuildDoctorInput): Promise<Doctor
|
|
|
717
547
|
}
|
|
718
548
|
}
|
|
719
549
|
|
|
720
|
-
// 7) Build
|
|
550
|
+
// 7) Build output
|
|
721
551
|
const out: DoctorOutput = {
|
|
722
552
|
status,
|
|
723
553
|
workspaceDir,
|
|
724
554
|
pdConfigPaths: {
|
|
725
555
|
workspaceDir: pathEntry(workspaceDir),
|
|
726
556
|
pdDir: pathEntry(pdDir),
|
|
727
|
-
|
|
728
|
-
workflowsYaml: pathEntry(workflowsPath),
|
|
557
|
+
configYaml: { ...pathEntry(configYamlPath), parseable: loadResult.ok || !fs.existsSync(configYamlPath) ? true : false },
|
|
729
558
|
stateDb: pathEntry(stateDbPath),
|
|
730
559
|
},
|
|
731
560
|
openclawConfigPaths: {
|
|
732
561
|
openclawHome: pathEntry(getOpenClawHome()),
|
|
733
562
|
openclawConfig: pathEntry(getOpenClawConfigPath()),
|
|
734
563
|
},
|
|
735
|
-
featureFlags
|
|
736
|
-
|
|
737
|
-
configPath: featureFlagsPath,
|
|
738
|
-
enabledMvpChannels,
|
|
739
|
-
disabledFlags,
|
|
740
|
-
warnings: flagWarnings,
|
|
741
|
-
},
|
|
564
|
+
featureFlags,
|
|
565
|
+
internalAgents,
|
|
742
566
|
providerHealth,
|
|
743
|
-
internalAgents: {
|
|
744
|
-
correctionObserver: {
|
|
745
|
-
enabled: correctionObserverEnabled,
|
|
746
|
-
flagSource: coFlagSource,
|
|
747
|
-
status: coStatus,
|
|
748
|
-
configSource: coConfigSource,
|
|
749
|
-
provider: coProvider,
|
|
750
|
-
model: coModel,
|
|
751
|
-
apiKeyEnv: coApiKeyEnv,
|
|
752
|
-
apiKeyPresent: coApiKeyPresent,
|
|
753
|
-
reason: coReason,
|
|
754
|
-
nextAction: coNextAction,
|
|
755
|
-
},
|
|
756
|
-
},
|
|
757
567
|
warnings,
|
|
758
|
-
nextActions: nextActions.length > 0 ? nextActions : ['All checks passed —
|
|
568
|
+
nextActions: nextActions.length > 0 ? nextActions : ['All checks passed — configuration is valid'],
|
|
569
|
+
legacyFilesDetected: loadResult.legacyFilesDetected,
|
|
759
570
|
};
|
|
760
571
|
|
|
761
572
|
if (reason) out.reason = reason;
|