@principles/pd-cli 1.101.0 → 1.103.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.
Files changed (39) hide show
  1. package/dist/commands/diagnose.js +27 -27
  2. package/dist/commands/diagnose.js.map +1 -1
  3. package/dist/commands/pain-retry.d.ts.map +1 -1
  4. package/dist/commands/pain-retry.js +22 -27
  5. package/dist/commands/pain-retry.js.map +1 -1
  6. package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
  7. package/dist/commands/runtime-internalization-integrity.js +40 -1
  8. package/dist/commands/runtime-internalization-integrity.js.map +1 -1
  9. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  10. package/dist/commands/runtime-internalization-run-once.js +11 -9
  11. package/dist/commands/runtime-internalization-run-once.js.map +1 -1
  12. package/dist/commands/runtime.d.ts +1 -1
  13. package/dist/commands/runtime.d.ts.map +1 -1
  14. package/dist/commands/runtime.js +92 -25
  15. package/dist/commands/runtime.js.map +1 -1
  16. package/dist/services/mainline-snapshot-assembler.d.ts +35 -0
  17. package/dist/services/mainline-snapshot-assembler.d.ts.map +1 -0
  18. package/dist/services/mainline-snapshot-assembler.js +399 -0
  19. package/dist/services/mainline-snapshot-assembler.js.map +1 -0
  20. package/dist/services/resolve-runtime-from-pd-config.d.ts +59 -0
  21. package/dist/services/resolve-runtime-from-pd-config.d.ts.map +1 -0
  22. package/dist/services/resolve-runtime-from-pd-config.js +96 -0
  23. package/dist/services/resolve-runtime-from-pd-config.js.map +1 -0
  24. package/package.json +1 -1
  25. package/src/commands/diagnose.ts +26 -26
  26. package/src/commands/pain-retry.ts +21 -25
  27. package/src/commands/runtime-internalization-integrity.ts +40 -1
  28. package/src/commands/runtime-internalization-run-once.ts +10 -9
  29. package/src/commands/runtime.ts +96 -24
  30. package/src/services/mainline-snapshot-assembler.ts +544 -0
  31. package/src/services/resolve-runtime-from-pd-config.ts +142 -0
  32. package/tests/commands/console-launcher-edge-cases.test.ts +14 -47
  33. package/tests/commands/diagnose.test.ts +91 -39
  34. package/tests/commands/pain-retry.test.ts +130 -15
  35. package/tests/commands/pri-393-runtime-config-unification.test.ts +284 -0
  36. package/tests/commands/runtime-internalization-integrity.test.ts +37 -0
  37. package/tests/commands/runtime-internalization-run-once.test.ts +59 -53
  38. package/tests/commands/runtime.test.ts +124 -1
  39. package/tests/services/mainline-snapshot-assembler.test.ts +425 -0
@@ -26,7 +26,6 @@ import {
26
26
  OpenClawCliRuntimeAdapter,
27
27
  PiAiRuntimeAdapter,
28
28
  PDRuntimeError,
29
- resolveRuntimeConfig,
30
29
  isRuntimeConfigError,
31
30
  CandidateIntakeService,
32
31
  run as diagnoseRun,
@@ -37,6 +36,7 @@ import { PrincipleTreeLedgerAdapter } from '../principle-tree-ledger-adapter.js'
37
36
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
38
37
  import { readOutputLanguageFromWorkspace } from '../config-reader.js';
39
38
  import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
39
+ import { resolveRuntimeFromPdConfig } from '../services/resolve-runtime-from-pd-config.js';
40
40
  import { isFeatureEnabled, SPLIT_PIPELINE_TOTAL_TIMEOUT_MS } from '@principles/core/runtime-v2';
41
41
  import * as path from 'path';
42
42
 
@@ -187,8 +187,8 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
187
187
 
188
188
  let runtimeAdapter: PDRuntimeAdapter;
189
189
  if (runtimeKind === 'openclaw-cli') {
190
- const stateDir = `${workspaceDir}/.state`;
191
- const configResult = resolveRuntimeConfig(stateDir, { openclawLocal: opts.openclawLocal, openclawGateway: opts.openclawGateway, requestedRuntimeKind: 'openclaw-cli' });
190
+ const resolved = resolveRuntimeFromPdConfig(workspaceDir);
191
+ const configResult = resolved.result;
192
192
  if (isRuntimeConfigError(configResult)) {
193
193
  if (opts.json) {
194
194
  console.log(JSON.stringify({ ok: false, reason: configResult.reason, message: configResult.message, nextAction: configResult.nextAction }));
@@ -200,19 +200,22 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
200
200
  return;
201
201
  }
202
202
  const { openclawMode } = configResult;
203
- if (!openclawMode) {
203
+ // CLI flags override config (PRI-393)
204
+ const flagMode = opts.openclawLocal ? 'local' as const : opts.openclawGateway ? 'gateway' as const : undefined;
205
+ const effectiveMode = flagMode ?? openclawMode;
206
+ if (!effectiveMode) {
204
207
  if (opts.json) {
205
- console.log(JSON.stringify({ ok: false, reason: 'missing_openclaw_mode', message: 'runtimeKind is openclaw-cli but no mode resolved', nextAction: 'Provide --openclaw-local or --openclaw-gateway, or set openclawMode in workflows.yaml' }));
208
+ console.log(JSON.stringify({ ok: false, reason: 'missing_openclaw_mode', message: 'runtimeKind is openclaw-cli but no mode resolved', nextAction: 'Provide --openclaw-local or --openclaw-gateway, or set openclawMode in .pd/config.yaml' }));
206
209
  } else {
207
210
  console.error('error: runtimeKind is openclaw-cli but no mode resolved');
208
- console.error('nextAction: Provide --openclaw-local or --openclaw-gateway, or set openclawMode in workflows.yaml');
211
+ console.error('nextAction: Provide --openclaw-local or --openclaw-gateway, or set openclawMode in .pd/config.yaml');
209
212
  }
210
213
  process.exit(1);
211
214
  return;
212
215
  }
213
216
 
214
217
  runtimeAdapter = new OpenClawCliRuntimeAdapter({
215
- runtimeMode: openclawMode,
218
+ runtimeMode: effectiveMode,
216
219
  workspaceDir,
217
220
  agentId: opts.agent ?? 'main',
218
221
  });
@@ -226,7 +229,7 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
226
229
  agentId: 'openclaw-cli-adapter',
227
230
  payload: {
228
231
  runtimeKind: 'openclaw-cli',
229
- runtimeMode: openclawMode,
232
+ runtimeMode: effectiveMode,
230
233
  },
231
234
  });
232
235
  } else if (runtimeKind === 'test-double') {
@@ -256,18 +259,14 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
256
259
  }),
257
260
  });
258
261
  } else if (runtimeKind === 'pi-ai') {
259
- const stateDir = `${workspaceDir}/.state`;
262
+ const resolved = resolveRuntimeFromPdConfig(workspaceDir);
263
+ for (const w of resolved.legacyWarnings) console.warn(`[pd diagnose] ${w}`);
264
+
260
265
  let policyConfig: RuntimeConfig | null = null;
261
- try {
262
- const configResult = resolveRuntimeConfig(stateDir);
263
- if (!isRuntimeConfigError(configResult)) {
264
- policyConfig = configResult;
265
- } else {
266
- console.warn(`[pd diagnose] workflows.yaml policy load failed: ${configResult.message}. Using CLI flags if provided.`);
267
- }
268
- } catch (err: unknown) {
269
- const detail = err instanceof Error ? err.message : String(err);
270
- console.warn(`[pd diagnose] workflows.yaml policy load failed: ${detail}. Using CLI flags if provided.`);
266
+ if (!isRuntimeConfigError(resolved.result)) {
267
+ policyConfig = resolved.result;
268
+ } else {
269
+ console.warn(`[pd diagnose] .pd/config.yaml resolution failed: ${resolved.result.message}. Using CLI flags if provided.`);
271
270
  }
272
271
 
273
272
  const provider = opts.provider ?? policyConfig?.provider;
@@ -285,15 +284,16 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
285
284
  if (missing.length > 0) {
286
285
  console.error(
287
286
  `error: missing required pi-ai config: ${missing.join(', ')}.\n` +
288
- `Pass via --flag or add to workflows.yaml pd-runtime-v2-diagnosis funnel policy.\n` +
287
+ `Pass via --flag or add to .pd/config.yaml runtime profile.\n` +
289
288
  `Example:\n` +
290
289
  ` pd diagnose run --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY\n` +
291
- ` Or add to workflows.yaml:\n` +
292
- ` policy:\n` +
293
- ` runtimeKind: pi-ai\n` +
294
- ` provider: openrouter\n` +
295
- ` model: anthropic/claude-sonnet-4\n` +
296
- ` apiKeyEnv: OPENROUTER_API_KEY`,
290
+ ` Or add to .pd/config.yaml:\n` +
291
+ ` runtimeProfiles:\n` +
292
+ ` - id: openrouter\n` +
293
+ ` type: pi-ai\n` +
294
+ ` provider: openrouter\n` +
295
+ ` model: anthropic/claude-sonnet-4\n` +
296
+ ` apiKeyEnv: OPENROUTER_API_KEY`,
297
297
  );
298
298
  process.exit(1);
299
299
  }
@@ -30,7 +30,6 @@ import {
30
30
  OpenClawCliRuntimeAdapter,
31
31
  PiAiRuntimeAdapter,
32
32
  PDRuntimeError,
33
- resolveRuntimeConfig,
34
33
  isRuntimeConfigError,
35
34
  CandidateIntakeService,
36
35
  run as diagnoseRun,
@@ -39,6 +38,7 @@ import {
39
38
  } from '@principles/core/runtime-v2';
40
39
  import type { PDRuntimeAdapter, RuntimeConfig, OutputLanguage } from '@principles/core/runtime-v2';
41
40
  import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
41
+ import { resolveRuntimeFromPdConfig } from '../services/resolve-runtime-from-pd-config.js';
42
42
  import type { PDTaskStatus } from '@principles/core/runtime-v2';
43
43
  import { PrincipleTreeLedgerAdapter } from '../principle-tree-ledger-adapter.js';
44
44
  import { readOutputLanguageFromWorkspace } from '../config-reader.js';
@@ -114,7 +114,6 @@ function refuseExit(opts: PainRetryOptions, payload: { status?: string; painId:
114
114
 
115
115
  export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
116
116
  const workspaceDir = resolveWorkspaceDir(opts.workspace);
117
- const stateDir = `${workspaceDir}/.state`;
118
117
 
119
118
  // Step 1: Resolve painId → taskId
120
119
  const resolution = resolveTaskIdFromPainId(opts.painId);
@@ -191,16 +190,12 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
191
190
  // P1 fix: pd pain retry must NOT default to test-double.
192
191
  // This command is for real workspace pain fixes — test-double would generate
193
192
  // fake candidates/ledger in a real .pd/state.db. Require explicit --runtime
194
- // or fall back to workflows.yaml config.
193
+ // or fall back to .pd/config.yaml.
195
194
  let runtimeKind = opts.runtime;
196
195
  if (!runtimeKind) {
197
- try {
198
- const configResult = resolveRuntimeConfig(stateDir);
199
- if (!isRuntimeConfigError(configResult) && configResult.runtimeKind) {
200
- ({ runtimeKind } = configResult);
201
- }
202
- } catch {
203
- // Config load failed — fall through to refusal
196
+ const resolved = resolveRuntimeFromPdConfig(workspaceDir);
197
+ if (!isRuntimeConfigError(resolved.result) && resolved.result.runtimeKind) {
198
+ ({ runtimeKind } = resolved.result);
204
199
  }
205
200
  }
206
201
 
@@ -209,7 +204,7 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
209
204
  painId: opts.painId,
210
205
  taskId,
211
206
  reason: 'missing_runtime',
212
- message: 'No --runtime specified and no workflows.yaml config found. pd pain retry must not default to test-double to prevent fake data in real workspaces.',
207
+ message: 'No --runtime specified and no .pd/config.yaml runtime binding found. pd pain retry must not default to test-double to prevent fake data in real workspaces.',
213
208
  nextAction: `Specify --runtime explicitly: pd pain retry --pain-id ${opts.painId} --runtime pi-ai --provider <provider> --model <model> --apiKeyEnv <ENV>`,
214
209
  });
215
210
  }
@@ -218,12 +213,16 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
218
213
  let runtimeAdapter: PDRuntimeAdapter;
219
214
 
220
215
  if (runtimeKind === 'openclaw-cli') {
221
- const configResult = resolveRuntimeConfig(stateDir, { openclawLocal: opts.openclawLocal, openclawGateway: opts.openclawGateway, requestedRuntimeKind: 'openclaw-cli' });
216
+ const resolved = resolveRuntimeFromPdConfig(workspaceDir);
217
+ const configResult = resolved.result;
222
218
  if (isRuntimeConfigError(configResult)) {
223
219
  refuseExit(opts, { painId: opts.painId, taskId, reason: configResult.reason, message: configResult.message, nextAction: configResult.nextAction });
224
220
  }
225
221
  const { openclawMode } = configResult;
226
- if (!openclawMode) {
222
+ // CLI flags override config (PRI-393)
223
+ const flagMode = opts.openclawLocal ? 'local' as const : opts.openclawGateway ? 'gateway' as const : undefined;
224
+ const effectiveMode = flagMode ?? openclawMode;
225
+ if (!effectiveMode) {
227
226
  refuseExit(opts, {
228
227
  painId: opts.painId,
229
228
  taskId,
@@ -234,7 +233,7 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
234
233
  }
235
234
 
236
235
  runtimeAdapter = new OpenClawCliRuntimeAdapter({
237
- runtimeMode: openclawMode,
236
+ runtimeMode: effectiveMode,
238
237
  workspaceDir,
239
238
  agentId: opts.agent ?? 'main',
240
239
  });
@@ -265,17 +264,14 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
265
264
  }),
266
265
  });
267
266
  } else if (runtimeKind === 'pi-ai') {
267
+ const resolved = resolveRuntimeFromPdConfig(workspaceDir);
268
+ for (const w of resolved.legacyWarnings) console.warn(`[pd pain retry] ${w}`);
269
+
268
270
  let policyConfig: RuntimeConfig | null = null;
269
- try {
270
- const configResult = resolveRuntimeConfig(stateDir);
271
- if (!isRuntimeConfigError(configResult)) {
272
- policyConfig = configResult;
273
- } else {
274
- console.warn(`[pd pain retry] workflows.yaml policy load failed: ${configResult.message}. Using CLI flags if provided.`);
275
- }
276
- } catch (err: unknown) {
277
- const detail = err instanceof Error ? err.message : String(err);
278
- console.warn(`[pd pain retry] workflows.yaml policy load failed: ${detail}. Using CLI flags if provided.`);
271
+ if (!isRuntimeConfigError(resolved.result)) {
272
+ policyConfig = resolved.result;
273
+ } else {
274
+ console.warn(`[pd pain retry] .pd/config.yaml resolution failed: ${resolved.result.message}. Using CLI flags if provided.`);
279
275
  }
280
276
 
281
277
  const provider = opts.provider ?? policyConfig?.provider;
@@ -296,7 +292,7 @@ export async function handlePainRetry(opts: PainRetryOptions): Promise<void> {
296
292
  taskId,
297
293
  reason: `missing_required_config: ${missing.join(', ')}`,
298
294
  message: `Missing or blank required pi-ai config: ${missing.join(', ')}`,
299
- nextAction: `Pass via --flag or add to workflows.yaml. Example: pd pain retry --pain-id ${opts.painId} --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY`,
295
+ nextAction: `Pass via --flag or add to .pd/config.yaml. Example: pd pain retry --pain-id ${opts.painId} --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY`,
300
296
  });
301
297
  }
302
298
 
@@ -2,6 +2,7 @@ import * as path from 'path';
2
2
  import { InternalizationChainIntegrityReadModel } from '@principles/core/runtime-v2';
3
3
  import type { ChainIntegrityResult } from '@principles/core/runtime-v2';
4
4
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
5
+ import { assembleMainlineSnapshot } from '../services/mainline-snapshot-assembler.js';
5
6
 
6
7
  interface InternalizationIntegrityOptions {
7
8
  workspace?: string;
@@ -47,7 +48,45 @@ export async function handleRuntimeInternalizationIntegrity(opts: Internalizatio
47
48
  ? path.resolve(opts.workspace)
48
49
  : resolveWorkspaceDir();
49
50
 
50
- const model = new InternalizationChainIntegrityReadModel({ workspaceDir });
51
+ let warnings: string[];
52
+ let snapshot;
53
+ try {
54
+ ({ snapshot, warnings } = await assembleMainlineSnapshot({ workspaceDir }));
55
+ } catch (error: unknown) {
56
+ const reason = error instanceof Error ? error.message : String(error);
57
+ const failure: ChainIntegrityResult = {
58
+ overallStatus: 'error',
59
+ brokenLinks: [{
60
+ type: 'mainline_snapshot_assembly_failed',
61
+ severity: 'error',
62
+ reason: `Failed to assemble mainline snapshot: ${reason}`,
63
+ recommendedAction: 'Verify workspace state.db/config and rerun `pd runtime internalization integrity`.',
64
+ }],
65
+ chainSummaries: {
66
+ totalCandidates: 0,
67
+ totalDreamerTasks: 0,
68
+ totalPhilosopherTasks: 0,
69
+ totalPIArtifacts: 0,
70
+ chainsWithBrokenLinks: 0,
71
+ },
72
+ generatedAt: new Date().toISOString(),
73
+ };
74
+ if (opts.json) {
75
+ console.log(JSON.stringify(failure, null, 2));
76
+ } else {
77
+ console.error(`FAIL: ${failure.brokenLinks[0].reason}`);
78
+ }
79
+ process.exitCode = 1;
80
+ return;
81
+ }
82
+
83
+ if (!opts.json && warnings.length > 0) {
84
+ for (const warning of warnings) {
85
+ console.warn(`Warning: ${warning}`);
86
+ }
87
+ }
88
+
89
+ const model = new InternalizationChainIntegrityReadModel({ workspaceDir, mainlineSnapshot: snapshot });
51
90
  const result = model.check();
52
91
 
53
92
  if (opts.json) {
@@ -20,13 +20,13 @@ import {
20
20
  TestDoubleRuntimeAdapter,
21
21
  PiAiRuntimeAdapter,
22
22
  OpenClawCliRuntimeAdapter,
23
- resolveRuntimeConfig,
24
23
  isRuntimeConfigError,
25
24
  validateRuntimeConfig,
26
25
  } from '@principles/core/runtime-v2';
27
26
  import type { WakeOnceResult, DreamerRunnerResult, PhilosopherRunnerResult, ScribeRunnerResult, ArtificerRunnerResult, EvaluatorRunnerResult, RolloutReviewerRunnerResult, TrainerRunnerResult, PDRuntimeAdapter, PeerRunnerKind, OutputLanguage } from '@principles/core/runtime-v2';
28
27
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
29
28
  import { readOutputLanguageFromWorkspace } from '../config-reader.js';
29
+ import { resolveRuntimeFromPdConfig } from '../services/resolve-runtime-from-pd-config.js';
30
30
 
31
31
  interface RunOnceOptions {
32
32
  workspace?: string;
@@ -472,19 +472,20 @@ function resolveRuntimeAdapter(opts: ResolveAdapterOptions): PDRuntimeAdapter {
472
472
  });
473
473
  }
474
474
 
475
- const stateDir = path.join(opts.workspaceDir, '.state');
476
- const configResult = resolveRuntimeConfig(stateDir, { requestedRuntimeKind: opts.runtimeKind });
475
+ // PRI-393: resolve runtime from .pd/config.yaml (not .state/workflows.yaml)
476
+ const resolved = resolveRuntimeFromPdConfig(opts.workspaceDir);
477
+ const configResult = resolved.result;
477
478
 
478
479
  if (isRuntimeConfigError(configResult)) {
479
480
  throw new ConfigResolutionError(
480
- `Config resolution failed: ${configResult.reason}. ` +
481
+ `Config resolution from .pd/config.yaml failed: ${configResult.reason}. ` +
481
482
  `${configResult.message}. nextAction: ${configResult.nextAction}`,
482
483
  );
483
484
  }
484
485
 
485
486
  if (opts.runtimeKind === 'pi-ai' || (opts.runtimeKind === 'config' && configResult.runtimeKind === 'pi-ai')) {
486
487
  validateRuntimeConfig(configResult);
487
- // CLI --timeout-ms overrides workflows.yaml timeoutMs
488
+ // CLI --timeout-ms overrides config timeoutMs
488
489
  const adapterTimeoutMs = opts.timeoutMs ?? configResult.timeoutMs;
489
490
  return new PiAiRuntimeAdapter({
490
491
  provider: String(configResult.provider),
@@ -502,8 +503,8 @@ function resolveRuntimeAdapter(opts: ResolveAdapterOptions): PDRuntimeAdapter {
502
503
  if (!openclawMode) {
503
504
  throw new ConfigResolutionError(
504
505
  `runtimeKind 'openclaw-cli' requires openclawMode. ` +
505
- `Provide --openclaw-local or --openclaw-gateway, or set openclawMode in workflows.yaml. ` +
506
- `nextAction: Add openclawMode: local|gateway to your funnel policy or use CLI flags.`,
506
+ `Provide --openclaw-local or --openclaw-gateway, or set openclawMode in .pd/config.yaml. ` +
507
+ `nextAction: Add openclawMode: local|gateway to your .pd/config.yaml runtime profile or use CLI flags.`,
507
508
  );
508
509
  }
509
510
  return new OpenClawCliRuntimeAdapter({
@@ -533,7 +534,7 @@ export async function handleRuntimeInternalizationRunOnce(opts: RunOnceOptions):
533
534
  if (runtimeKind === 'test-double' && !opts.allowTestDouble) {
534
535
  console.error('Error: test-double runtime mutates real queue state (leases tasks, marks them succeeded with empty output).');
535
536
  console.error('Use --runtime test-double --allow-test-double to acknowledge this risk.');
536
- console.error('For production use, use --runtime config (reads from workflows.yaml) or --runtime pi-ai / openclaw-cli.');
537
+ console.error('For production use, use --runtime config (reads from .pd/config.yaml) or --runtime pi-ai / openclaw-cli.');
537
538
  process.exitCode = 1;
538
539
  return;
539
540
  }
@@ -716,7 +717,7 @@ export async function handleRuntimeInternalizationRunOnce(opts: RunOnceOptions):
716
717
  decision: isConfigError ? 'config_error' : 'runtime_error',
717
718
  reason: message,
718
719
  nextAction: isConfigError
719
- ? 'Fix the workflows.yaml funnel policy, or use --runtime pi-ai / openclaw-cli with explicit flags'
720
+ ? 'Fix the .pd/config.yaml runtime profile, or use --runtime pi-ai / openclaw-cli with explicit flags'
720
721
  : 'Check runner logs and workspace state; re-run with --runtime test-double to isolate',
721
722
  }, null, 2));
722
723
  } else {
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import * as path from 'path';
11
11
  import { probeRuntime } from '@principles/core/runtime-v2';
12
- import { PDRuntimeError } from '@principles/core/runtime-v2';
12
+ import { PDRuntimeError, isRuntimeConfigError } from '@principles/core/runtime-v2';
13
+ import { resolveRuntimeFromPdConfig, resolveRuntimeWithOverrides } from '../services/resolve-runtime-from-pd-config.js';
13
14
 
14
15
  interface RuntimeProbeOptions {
15
16
  runtime: string;
@@ -139,48 +140,55 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
139
140
  let model = opts.model ?? '';
140
141
  let apiKeyEnv = opts.apiKeyEnv ?? '';
141
142
  let baseUrl = opts.baseUrl ?? '';
142
- let {timeoutMs} = opts;
143
+ let { timeoutMs, maxRetries } = opts;
143
144
 
144
- // D-01: always load workspace policy; CLI values take priority as override
145
+ // PRI-393: always load workspace policy from .pd/config.yaml (not .state/workflows.yaml)
145
146
  if (workspaceDir) {
146
- try {
147
- const { resolveRuntimeConfig, isRuntimeConfigError } = await import('@principles/core/runtime-v2');
148
- const configResult = resolveRuntimeConfig(path.join(workspaceDir, '.state'));
149
- if (isRuntimeConfigError(configResult)) {
150
- console.warn(`Warning: could not load workspace runtime config — ${configResult.message}`);
151
- } else {
152
- const config = configResult;
153
- provider = provider || config.provider || '';
154
- model = model || config.model || '';
155
- apiKeyEnv = apiKeyEnv || config.apiKeyEnv || '';
156
- baseUrl = baseUrl || config.baseUrl || '';
157
- timeoutMs = timeoutMs ?? config.timeoutMs;
158
- }
159
- } catch (err) {
160
- console.warn(`Warning: could not load workspace runtime config — policy fallback disabled: ${err instanceof Error ? err.message : String(err)}`);
147
+ const resolved = resolveRuntimeWithOverrides(workspaceDir, {
148
+ provider: opts.provider,
149
+ model: opts.model,
150
+ apiKeyEnv: opts.apiKeyEnv,
151
+ baseUrl: opts.baseUrl,
152
+ maxRetries: opts.maxRetries,
153
+ timeoutMs: opts.timeoutMs,
154
+ });
155
+ for (const w of resolved.legacyWarnings) console.warn(`Warning: ${w}`);
156
+ if (resolved.mergedConfig) {
157
+ provider = provider || resolved.mergedConfig.provider || '';
158
+ model = model || resolved.mergedConfig.model || '';
159
+ apiKeyEnv = apiKeyEnv || resolved.mergedConfig.apiKeyEnv || '';
160
+ baseUrl = baseUrl || resolved.mergedConfig.baseUrl || '';
161
+ timeoutMs = timeoutMs ?? resolved.mergedConfig.timeoutMs;
162
+ maxRetries = maxRetries ?? resolved.mergedConfig.maxRetries;
163
+ } else if (isRuntimeConfigError(resolved.result)) {
164
+ console.warn(`Warning: could not resolve runtime from .pd/config.yaml — ${resolved.result.message}`);
161
165
  }
162
166
  }
163
167
 
164
168
  if (!provider) {
165
- console.error("error: --provider is required for --runtime pi-ai (or set in --workspace workflows.yaml)");
169
+ console.error("error: --provider is required for --runtime pi-ai (or set in .pd/config.yaml)");
166
170
  console.error(" e.g.: pd runtime probe --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY");
167
171
  process.exit(1);
172
+ return;
168
173
  }
169
174
  if (!model) {
170
- console.error("error: --model is required for --runtime pi-ai (or set in --workspace workflows.yaml)");
175
+ console.error("error: --model is required for --runtime pi-ai (or set in .pd/config.yaml)");
171
176
  console.error(" e.g.: pd runtime probe --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY");
172
177
  process.exit(1);
178
+ return;
173
179
  }
174
180
  if (!apiKeyEnv) {
175
- console.error("error: --apiKeyEnv is required for --runtime pi-ai (or set in --workspace workflows.yaml)");
181
+ console.error("error: --apiKeyEnv is required for --runtime pi-ai (or set in .pd/config.yaml)");
176
182
  console.error(" e.g.: pd runtime probe --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY");
177
183
  process.exit(1);
184
+ return;
178
185
  }
179
186
 
180
187
  // D-09: check env var exists before calling probeRuntime
181
188
  if (!process.env[apiKeyEnv]) {
182
189
  console.error(`error: environment variable '${apiKeyEnv}' is not set`);
183
190
  process.exit(1);
191
+ return;
184
192
  }
185
193
 
186
194
  try {
@@ -190,7 +198,7 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
190
198
  model,
191
199
  apiKeyEnv,
192
200
  baseUrl,
193
- maxRetries: opts.maxRetries,
201
+ maxRetries: maxRetries,
194
202
  timeoutMs: timeoutMs ?? 120_000, // D-04: probe timeout 120s (matches Runtime defaults)
195
203
  });
196
204
 
@@ -265,7 +273,66 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
265
273
  }
266
274
 
267
275
  /**
268
- * pd runtime probe — dispatches to openclaw-cli or pi-ai branch.
276
+ * --runtime config probe branch PRI-393
277
+ * Resolves runtime from .pd/config.yaml, then dispatches to pi-ai or openclaw-cli probe.
278
+ */
279
+ async function handleConfigProbe(opts: RuntimeProbeOptions): Promise<void> {
280
+ const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : undefined;
281
+ if (!workspaceDir) {
282
+ console.error('error: --workspace is required for --runtime config');
283
+ process.exit(1);
284
+ return;
285
+ }
286
+
287
+ const resolved = resolveRuntimeFromPdConfig(workspaceDir);
288
+ for (const w of resolved.legacyWarnings) console.warn(`Warning: ${w}`);
289
+
290
+ if (isRuntimeConfigError(resolved.result)) {
291
+ if (opts.json) {
292
+ console.log(JSON.stringify({
293
+ status: 'failed',
294
+ errorCategory: 'config_error',
295
+ message: resolved.result.message,
296
+ reason: resolved.result.reason,
297
+ nextAction: resolved.result.nextAction,
298
+ configSource: resolved.configSource,
299
+ }, null, 2));
300
+ } else {
301
+ console.error(`error: ${resolved.result.message}`);
302
+ console.error(`nextAction: ${resolved.result.nextAction}`);
303
+ }
304
+ process.exit(1);
305
+ return;
306
+ }
307
+
308
+ const config = resolved.result;
309
+ // Dispatch to the appropriate runtime probe
310
+ if (config.runtimeKind === 'pi-ai') {
311
+ return handlePiAiProbe({
312
+ ...opts,
313
+ provider: opts.provider ?? config.provider,
314
+ model: opts.model ?? config.model,
315
+ apiKeyEnv: opts.apiKeyEnv ?? config.apiKeyEnv,
316
+ baseUrl: opts.baseUrl ?? config.baseUrl,
317
+ timeoutMs: opts.timeoutMs ?? config.timeoutMs,
318
+ });
319
+ }
320
+
321
+ if (config.runtimeKind === 'openclaw-cli') {
322
+ return handleOpenClawProbe({
323
+ ...opts,
324
+ openclawLocal: config.openclawMode === 'local' ? true : opts.openclawLocal,
325
+ openclawGateway: config.openclawMode === 'gateway' ? true : opts.openclawGateway,
326
+ });
327
+ }
328
+
329
+ console.error(`error: unsupported runtimeKind '${config.runtimeKind}' from .pd/config.yaml`);
330
+ process.exit(1);
331
+ return;
332
+ }
333
+
334
+ /**
335
+ * pd runtime probe — dispatches to openclaw-cli, pi-ai, or config branch.
269
336
  */
270
337
  export async function handleRuntimeProbe(opts: RuntimeProbeOptions): Promise<void> {
271
338
  if (opts.runtime === 'openclaw-cli') {
@@ -276,6 +343,11 @@ export async function handleRuntimeProbe(opts: RuntimeProbeOptions): Promise<voi
276
343
  return handlePiAiProbe(opts);
277
344
  }
278
345
 
279
- console.error(`error: unsupported --runtime '${opts.runtime}' (supported: openclaw-cli, pi-ai)`);
346
+ if (opts.runtime === 'config') {
347
+ return handleConfigProbe(opts);
348
+ }
349
+
350
+ console.error(`error: unsupported --runtime '${opts.runtime}' (supported: openclaw-cli, pi-ai, config)`);
280
351
  process.exit(1);
352
+ return;
281
353
  }