@principles/pd-cli 1.108.0 → 1.108.1

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.
@@ -8,8 +8,8 @@
8
8
  * HG-01 HARD GATE: This command must deliver.
9
9
  */
10
10
  import * as path from 'path';
11
- import { probeRuntime } from '@principles/core/runtime-v2';
12
- import { PDRuntimeError, isRuntimeConfigError } from '@principles/core/runtime-v2';
11
+ import type { Command } from 'commander';
12
+ import { probeRuntime, PDRuntimeError, isRuntimeConfigError } from '@principles/core/runtime-v2';
13
13
  import { resolveRuntimeFromPdConfig, resolveRuntimeWithOverrides } from '../services/resolve-runtime-from-pd-config.js';
14
14
 
15
15
  interface RuntimeProbeOptions {
@@ -132,6 +132,11 @@ async function handleOpenClawProbe(opts: RuntimeProbeOptions): Promise<void> {
132
132
 
133
133
  /**
134
134
  * pi-ai probe branch — validates flags, calls probeRuntime, formats output.
135
+ *
136
+ * PRI-402: When --workspace is provided without explicit --provider,
137
+ * reads pi-ai config from .pd/config.yaml via resolveRuntimeWithOverrides.
138
+ * JSON output includes configSource, runtimeProfileId, runtimeProfileLabel
139
+ * for alignment with `pd config doctor`.
135
140
  */
136
141
  async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
137
142
  // D-01: flags are required for pi-ai probe unless --workspace is provided (policy fallback)
@@ -142,6 +147,11 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
142
147
  let baseUrl = opts.baseUrl ?? '';
143
148
  let { timeoutMs, maxRetries } = opts;
144
149
 
150
+ // PRI-402: Track config source and profile info for JSON output
151
+ let configSource: string | null = null;
152
+ let runtimeProfileId: string | null = null;
153
+ let runtimeProfileLabel: string | null = null;
154
+
145
155
  // PRI-393: always load workspace policy from .pd/config.yaml (not .state/workflows.yaml)
146
156
  if (workspaceDir) {
147
157
  const resolved = resolveRuntimeWithOverrides(workspaceDir, {
@@ -153,6 +163,10 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
153
163
  timeoutMs: opts.timeoutMs,
154
164
  });
155
165
  for (const w of resolved.legacyWarnings) console.warn(`Warning: ${w}`);
166
+
167
+ // PRI-402: capture profile info regardless of merge result
168
+ ({ configSource, runtimeProfileId, runtimeProfileLabel } = resolved);
169
+
156
170
  if (resolved.mergedConfig) {
157
171
  provider = provider || resolved.mergedConfig.provider || '';
158
172
  model = model || resolved.mergedConfig.model || '';
@@ -161,32 +175,92 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
161
175
  timeoutMs = timeoutMs ?? resolved.mergedConfig.timeoutMs;
162
176
  maxRetries = maxRetries ?? resolved.mergedConfig.maxRetries;
163
177
  } else if (isRuntimeConfigError(resolved.result)) {
164
- console.warn(`Warning: could not resolve runtime from .pd/config.yaml ${resolved.result.message}`);
178
+ // PRI-402: fail-loud JSON when config.yaml is broken (EP-03, EP-04)
179
+ if (opts.json) {
180
+ console.log(JSON.stringify({
181
+ ok: false,
182
+ status: 'failed',
183
+ reason: resolved.result.reason,
184
+ message: resolved.result.message,
185
+ nextAction: resolved.result.nextAction,
186
+ configSource: resolved.configSource,
187
+ }, null, 2));
188
+ } else {
189
+ console.error(`error: could not resolve runtime from .pd/config.yaml — ${resolved.result.message}`);
190
+ console.error(`nextAction: ${resolved.result.nextAction}`);
191
+ }
192
+ process.exit(1);
193
+ return;
165
194
  }
166
195
  }
167
196
 
168
197
  if (!provider) {
169
- console.error("error: --provider is required for --runtime pi-ai (or set in .pd/config.yaml)");
170
- console.error(" e.g.: pd runtime probe --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY");
198
+ // PRI-402: fail-loud JSON when provider is missing (EP-03, EP-04)
199
+ if (opts.json) {
200
+ console.log(JSON.stringify({
201
+ ok: false,
202
+ status: 'failed',
203
+ reason: 'provider_missing',
204
+ message: '--provider is required for --runtime pi-ai (or set in .pd/config.yaml)',
205
+ nextAction: 'Set provider in .pd/config.yaml runtimeProfiles, or pass --provider explicitly',
206
+ configSource,
207
+ }, null, 2));
208
+ } else {
209
+ console.error("error: --provider is required for --runtime pi-ai (or set in .pd/config.yaml)");
210
+ console.error(" e.g.: pd runtime probe --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY");
211
+ }
171
212
  process.exit(1);
172
213
  return;
173
214
  }
174
215
  if (!model) {
175
- console.error("error: --model is required for --runtime pi-ai (or set in .pd/config.yaml)");
176
- console.error(" e.g.: pd runtime probe --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY");
216
+ if (opts.json) {
217
+ console.log(JSON.stringify({
218
+ ok: false,
219
+ status: 'failed',
220
+ reason: 'model_missing',
221
+ message: '--model is required for --runtime pi-ai (or set in .pd/config.yaml)',
222
+ nextAction: 'Set model in .pd/config.yaml runtimeProfiles, or pass --model explicitly',
223
+ configSource,
224
+ }, null, 2));
225
+ } else {
226
+ console.error("error: --model is required for --runtime pi-ai (or set in .pd/config.yaml)");
227
+ console.error(" e.g.: pd runtime probe --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY");
228
+ }
177
229
  process.exit(1);
178
230
  return;
179
231
  }
180
232
  if (!apiKeyEnv) {
181
- console.error("error: --apiKeyEnv is required for --runtime pi-ai (or set in .pd/config.yaml)");
182
- console.error(" e.g.: pd runtime probe --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY");
233
+ if (opts.json) {
234
+ console.log(JSON.stringify({
235
+ ok: false,
236
+ status: 'failed',
237
+ reason: 'apiKeyEnv_missing',
238
+ message: '--apiKeyEnv is required for --runtime pi-ai (or set in .pd/config.yaml)',
239
+ nextAction: 'Set apiKeyEnv in .pd/config.yaml runtimeProfiles, or pass --apiKeyEnv explicitly',
240
+ configSource,
241
+ }, null, 2));
242
+ } else {
243
+ console.error("error: --apiKeyEnv is required for --runtime pi-ai (or set in .pd/config.yaml)");
244
+ console.error(" e.g.: pd runtime probe --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY");
245
+ }
183
246
  process.exit(1);
184
247
  return;
185
248
  }
186
249
 
187
250
  // D-09: check env var exists before calling probeRuntime
188
251
  if (!process.env[apiKeyEnv]) {
189
- console.error(`error: environment variable '${apiKeyEnv}' is not set`);
252
+ if (opts.json) {
253
+ console.log(JSON.stringify({
254
+ ok: false,
255
+ status: 'failed',
256
+ reason: 'api_key_not_set',
257
+ message: `Environment variable '${apiKeyEnv}' is not set`,
258
+ nextAction: `Set the environment variable '${apiKeyEnv}' with a valid API key`,
259
+ configSource,
260
+ }, null, 2));
261
+ } else {
262
+ console.error(`error: environment variable '${apiKeyEnv}' is not set`);
263
+ }
190
264
  process.exit(1);
191
265
  return;
192
266
  }
@@ -215,7 +289,9 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
215
289
  if (!result.health.healthy) exitCode = 1;
216
290
 
217
291
  if (opts.json) {
218
- console.log(JSON.stringify({
292
+ // PRI-402: include configSource, runtimeProfileId, runtimeProfileLabel in JSON output
293
+ const jsonOutput: Record<string, unknown> = {
294
+ ok: result.health.healthy,
219
295
  status,
220
296
  runtimeKind: result.runtimeKind,
221
297
  provider: result.provider,
@@ -223,7 +299,11 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
223
299
  baseUrlPresent: !!baseUrl,
224
300
  health: result.health,
225
301
  capabilities: result.capabilities,
226
- }, null, 2));
302
+ };
303
+ if (configSource) jsonOutput.configSource = configSource;
304
+ if (runtimeProfileId) jsonOutput.runtimeProfileId = runtimeProfileId;
305
+ if (runtimeProfileLabel) jsonOutput.runtimeProfileLabel = runtimeProfileLabel;
306
+ console.log(JSON.stringify(jsonOutput, null, 2));
227
307
  if (exitCode !== 0) process.exit(exitCode);
228
308
  return;
229
309
  }
@@ -233,6 +313,8 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
233
313
  console.log(`Provider: ${result.provider}`);
234
314
  console.log(`Model: ${result.model}`);
235
315
  if (baseUrl) console.log(`BaseUrl: ${baseUrl}`);
316
+ if (runtimeProfileLabel) console.log(`Profile: ${runtimeProfileLabel}`);
317
+ if (configSource) console.log(`Config: ${configSource}`);
236
318
  console.log(`Status: ${status}`);
237
319
  console.log('');
238
320
  console.log('Health:');
@@ -260,10 +342,12 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
260
342
  }
261
343
  if (opts.json) {
262
344
  console.log(JSON.stringify({
345
+ ok: false,
263
346
  status: 'failed',
264
347
  errorCategory,
265
348
  message,
266
349
  runtimeKind: 'pi-ai',
350
+ configSource,
267
351
  }, null, 2));
268
352
  } else {
269
353
  console.error(`error: ${message} (${errorCategory})`);
@@ -279,7 +363,17 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
279
363
  async function handleConfigProbe(opts: RuntimeProbeOptions): Promise<void> {
280
364
  const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : undefined;
281
365
  if (!workspaceDir) {
282
- console.error('error: --workspace is required for --runtime config');
366
+ if (opts.json) {
367
+ console.log(JSON.stringify({
368
+ ok: false,
369
+ status: 'failed',
370
+ reason: 'workspace_missing',
371
+ message: '--workspace is required for --runtime config',
372
+ nextAction: 'Provide --workspace <path> pointing to a PD workspace directory',
373
+ }, null, 2));
374
+ } else {
375
+ console.error('error: --workspace is required for --runtime config');
376
+ }
283
377
  process.exit(1);
284
378
  return;
285
379
  }
@@ -290,6 +384,7 @@ async function handleConfigProbe(opts: RuntimeProbeOptions): Promise<void> {
290
384
  if (isRuntimeConfigError(resolved.result)) {
291
385
  if (opts.json) {
292
386
  console.log(JSON.stringify({
387
+ ok: false,
293
388
  status: 'failed',
294
389
  errorCategory: 'config_error',
295
390
  message: resolved.result.message,
@@ -351,3 +446,28 @@ export async function handleRuntimeProbe(opts: RuntimeProbeOptions): Promise<voi
351
446
  process.exit(1);
352
447
  return;
353
448
  }
449
+
450
+ /**
451
+ * Register the `pd runtime probe` command on a Commander instance.
452
+ * Used by index.ts and tests to ensure real command wiring is verified (EP-04).
453
+ */
454
+ export function registerRuntimeProbeCommand(runtimeCmd: Command): Command {
455
+ return runtimeCmd
456
+ .command('probe')
457
+ .description('Probe runtime health and capabilities (HG-01 HARD GATE)')
458
+ .requiredOption('-r, --runtime <kind>', "Runtime kind: 'openclaw-cli', 'pi-ai', or 'config'")
459
+ .option('--openclaw-local', 'Use local OpenClaw (mutually exclusive with --openclaw-gateway)')
460
+ .option('--openclaw-gateway', 'Use gateway OpenClaw (mutually exclusive with --openclaw-local)')
461
+ .option('-a, --agent <agentId>', 'Agent ID to probe')
462
+ .option('--provider <name>', 'LLM provider (e.g., openrouter) \u2014 for pi-ai, falls back to .pd/config.yaml')
463
+ .option('--model <id>', 'Model ID (e.g., anthropic/claude-sonnet-4) \u2014 for pi-ai, falls back to .pd/config.yaml')
464
+ .option('--apiKeyEnv <name>', 'Env var name for API key (e.g., OPENROUTER_API_KEY) \u2014 for pi-ai, falls back to .pd/config.yaml')
465
+ .option('--baseUrl <url>', 'Custom base URL for OpenAI-compatible providers \u2014 for pi-ai, falls back to .pd/config.yaml')
466
+ .option('--maxRetries <n>', 'Max retry attempts for LLM failures', parseInt)
467
+ .option('--timeoutMs <ms>', 'Timeout in milliseconds for probe', parseInt)
468
+ .option('-w, --workspace <path>', 'Workspace directory \u2014 loads pi-ai config from .pd/config.yaml')
469
+ .option('--json', 'Output raw JSON')
470
+ .action(async (opts) => {
471
+ await handleRuntimeProbe(opts);
472
+ });
473
+ }
package/src/index.ts CHANGED
@@ -24,7 +24,7 @@ import { handleContextBuild } from './commands/context.js';
24
24
  import { handleLegacyImportOpenClaw } from './commands/legacy-import.js';
25
25
  import { handleLegacyCleanup } from './commands/legacy-cleanup.js';
26
26
  import { handleDiagnoseStatus, handleDiagnoseRun } from './commands/diagnose.js';
27
- import { handleRuntimeProbe } from './commands/runtime.js';
27
+ import { registerRuntimeProbeCommand } from './commands/runtime.js';
28
28
  import { handleFlowShow } from './commands/flow.js';
29
29
  import { handleTraceShow } from './commands/trace.js';
30
30
  import { handlePruningReport, handlePruningExplain, handlePruningReview, handlePruningRollback, handlePruningOrphans } from './commands/runtime-pruning.js';
@@ -430,24 +430,7 @@ demoCmd
430
430
  });
431
431
  });
432
432
 
433
- runtimeCmd
434
- .command('probe')
435
- .description('Probe runtime health and capabilities (HG-01 HARD GATE)')
436
- .requiredOption('-r, --runtime <kind>', "Runtime kind: 'openclaw-cli' or 'pi-ai'")
437
- .option('--openclaw-local', 'Use local OpenClaw (mutually exclusive with --openclaw-gateway)')
438
- .option('--openclaw-gateway', 'Use gateway OpenClaw (mutually exclusive with --openclaw-local)')
439
- .option('-a, --agent <agentId>', 'Agent ID to probe')
440
- .option('--provider <name>', 'LLM provider (e.g., openrouter) 鈥?for pi-ai, falls back to --workspace workflows.yaml')
441
- .option('--model <id>', 'Model ID (e.g., anthropic/claude-sonnet-4) 鈥?for pi-ai, falls back to --workspace workflows.yaml')
442
- .option('--apiKeyEnv <name>', 'Env var name for API key (e.g., OPENROUTER_API_KEY) 鈥?for pi-ai, falls back to --workspace workflows.yaml')
443
- .option('--baseUrl <url>', 'Custom base URL for OpenAI-compatible providers 鈥?for pi-ai, falls back to --workspace workflows.yaml')
444
- .option('--maxRetries <n>', 'Max retry attempts for LLM failures', parseInt)
445
- .option('--timeoutMs <ms>', 'Timeout in milliseconds for probe', parseInt)
446
- .option('-w, --workspace <path>', 'Workspace directory 鈥?loads pi-ai policy from .state/workflows.yaml')
447
- .option('--json', 'Output raw JSON')
448
- .action(async (opts) => {
449
- await handleRuntimeProbe(opts);
450
- });
433
+ registerRuntimeProbeCommand(runtimeCmd);
451
434
 
452
435
  const flowCmd = runtimeCmd
453
436
  .command('flow')
@@ -20,6 +20,7 @@ import {
20
20
  assertMainlineContract,
21
21
  EMPTY_CONTEXT_SENTINEL,
22
22
  hydratePITaskRecord,
23
+ resolveAgentRuntimeBinding,
23
24
  type MainlineSnapshot,
24
25
  type RuntimeReadinessSnapshot,
25
26
  type DiagnosisTaskSnapshot,
@@ -140,13 +141,32 @@ function buildDefaultReadiness(workspaceDir: string, warnings: string[]): Runtim
140
141
  warnings.push(...configLoadResult.warnings);
141
142
  }
142
143
 
144
+ // PRI-402: resolve real runtimeProbeProfile from .pd/config.yaml
145
+ // so smoke's config_source_alignment + diagnostician_readiness can judge correctly
146
+ let runtimeProbeProfile: string | null = null;
147
+ let diagnosticianReady = false;
148
+ let diagnosticianReadinessReason = 'No readiness snapshot provided; run "pd runtime probe" first.';
149
+
150
+ if (configLoadResult.ok) {
151
+ const bindingResult = resolveAgentRuntimeBinding(configLoadResult.effective, 'diagnostician');
152
+ if (bindingResult.ok) {
153
+ runtimeProbeProfile = bindingResult.profileId;
154
+ // Profile exists and is bound — readiness is at least "configured"
155
+ // Actual connectivity is unknown without probe, but config alignment is satisfied
156
+ diagnosticianReady = true;
157
+ diagnosticianReadinessReason = `Profile '${bindingResult.profileId}' resolved from .pd/config.yaml (connectivity not probed)`;
158
+ } else {
159
+ diagnosticianReadinessReason = bindingResult.reason;
160
+ }
161
+ }
162
+
143
163
  return {
144
164
  configDoctorProfile: defaultProfile,
145
- runtimeProbeProfile: null,
165
+ runtimeProbeProfile,
146
166
  configSource: '.pd/config.yaml',
147
167
  probeConfigSource: '.pd/config.yaml',
148
- diagnosticianReady: false,
149
- diagnosticianReadinessReason: 'No readiness snapshot provided; run "pd runtime probe" first.',
168
+ diagnosticianReady,
169
+ diagnosticianReadinessReason,
150
170
  };
151
171
  }
152
172
 
@@ -16,6 +16,7 @@
16
16
  import {
17
17
  resolveRuntimeConfigFromPdConfig,
18
18
  isRuntimeConfigError,
19
+ resolveAgentRuntimeBinding,
19
20
  } from '@principles/core/runtime-v2';
20
21
  import type {
21
22
  RuntimeConfigResult,
@@ -34,6 +35,33 @@ export interface ResolvedRuntimeFromPdConfig {
34
35
  configLoadResult: PdConfigLoadResult;
35
36
  /** Canonical config source label, e.g. ".pd/config.yaml". */
36
37
  configSource: string;
38
+ /**
39
+ * PRI-402: Resolved runtime profile ID for the diagnostician agent.
40
+ * e.g. "pi-ai.lmstudio". null when config resolution fails or profile is not found.
41
+ */
42
+ runtimeProfileId: string | null;
43
+ /**
44
+ * PRI-402: Human-readable runtime profile label for the diagnostician agent.
45
+ * e.g. "pi-ai: lmstudio/qwen3.6-27b-mtp". null when config resolution fails.
46
+ * Matches the label format used by `pd config doctor`.
47
+ */
48
+ runtimeProfileLabel: string | null;
49
+ }
50
+
51
+ /**
52
+ * Build a profile label matching the format used by `pd config doctor`.
53
+ * Mirrors `buildProfileLabel` in `pd-config-redaction.ts` (core).
54
+ */
55
+ function buildProfileLabel(profileId: string, profile: { type: string; provider?: string; model?: string; source?: string }): string {
56
+ if (profile.type === 'openclaw') {
57
+ const parts: string[] = ['openclaw'];
58
+ if (profile.provider) parts.push(profile.provider);
59
+ if (profile.model) parts.push(profile.model);
60
+ if (profile.source && !profile.provider && !profile.model) parts.push(profile.source);
61
+ return parts.join(': ');
62
+ }
63
+ // pi-ai
64
+ return `pi-ai: ${profile.provider ?? 'unknown'}/${profile.model ?? 'unknown'}`;
37
65
  }
38
66
 
39
67
  /**
@@ -77,11 +105,22 @@ export function resolveRuntimeFromPdConfig(
77
105
  legacyWarnings,
78
106
  configLoadResult,
79
107
  configSource: '.pd/config.yaml',
108
+ runtimeProfileId: null,
109
+ runtimeProfileLabel: null,
80
110
  };
81
111
  }
82
112
 
83
113
  const result = resolveRuntimeConfigFromPdConfig(configLoadResult.effective, getEnvVar);
84
114
 
115
+ // PRI-402: Extract profile ID and label for probe output alignment with doctor
116
+ let runtimeProfileId: string | null = null;
117
+ let runtimeProfileLabel: string | null = null;
118
+ const bindingResult = resolveAgentRuntimeBinding(configLoadResult.effective, 'diagnostician');
119
+ if (bindingResult.ok) {
120
+ runtimeProfileId = bindingResult.profileId;
121
+ runtimeProfileLabel = buildProfileLabel(bindingResult.profileId, bindingResult.profile);
122
+ }
123
+
85
124
  const legacyWarnings = configLoadResult.legacyFilesDetected.length > 0
86
125
  ? [
87
126
  `Legacy config files detected: ${configLoadResult.legacyFilesDetected.join(', ')}. ` +
@@ -94,6 +133,8 @@ export function resolveRuntimeFromPdConfig(
94
133
  legacyWarnings,
95
134
  configLoadResult,
96
135
  configSource: '.pd/config.yaml',
136
+ runtimeProfileId,
137
+ runtimeProfileLabel,
97
138
  };
98
139
  }
99
140