@principles/pd-cli 1.108.0 → 1.109.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/__tests__/runtime-probe-config.test.d.ts +20 -0
  2. package/dist/commands/__tests__/runtime-probe-config.test.d.ts.map +1 -0
  3. package/dist/commands/__tests__/runtime-probe-config.test.js +388 -0
  4. package/dist/commands/__tests__/runtime-probe-config.test.js.map +1 -0
  5. package/dist/commands/config-doctor.d.ts.map +1 -1
  6. package/dist/commands/config-doctor.js +4 -0
  7. package/dist/commands/config-doctor.js.map +1 -1
  8. package/dist/commands/runtime.d.ts +6 -0
  9. package/dist/commands/runtime.d.ts.map +1 -1
  10. package/dist/commands/runtime.js +139 -13
  11. package/dist/commands/runtime.js.map +1 -1
  12. package/dist/index.js +2 -19
  13. package/dist/index.js.map +1 -1
  14. package/dist/services/config-doctor.d.ts +2 -0
  15. package/dist/services/config-doctor.d.ts.map +1 -1
  16. package/dist/services/config-doctor.js +3 -0
  17. package/dist/services/config-doctor.js.map +1 -1
  18. package/dist/services/mainline-snapshot-assembler.d.ts.map +1 -1
  19. package/dist/services/mainline-snapshot-assembler.js +22 -4
  20. package/dist/services/mainline-snapshot-assembler.js.map +1 -1
  21. package/dist/services/pd-config-loader.d.ts +4 -0
  22. package/dist/services/pd-config-loader.d.ts.map +1 -1
  23. package/dist/services/pd-config-loader.js +12 -0
  24. package/dist/services/pd-config-loader.js.map +1 -1
  25. package/dist/services/resolve-runtime-from-pd-config.d.ts +11 -0
  26. package/dist/services/resolve-runtime-from-pd-config.d.ts.map +1 -1
  27. package/dist/services/resolve-runtime-from-pd-config.js +31 -1
  28. package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/commands/__tests__/runtime-probe-config.test.ts +431 -0
  31. package/src/commands/config-doctor.ts +4 -0
  32. package/src/commands/runtime.ts +133 -13
  33. package/src/index.ts +2 -19
  34. package/src/services/config-doctor.ts +6 -0
  35. package/src/services/mainline-snapshot-assembler.ts +23 -3
  36. package/src/services/pd-config-loader.ts +17 -0
  37. package/src/services/resolve-runtime-from-pd-config.ts +41 -0
  38. package/tests/commands/config-doctor.test.ts +15 -0
  39. package/tests/services/pd-config-loader.test.ts +21 -0
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')
@@ -110,6 +110,8 @@ export interface DoctorOutput {
110
110
  nextActions: string[];
111
111
  /** Legacy files detected (informational only, not used for resolution) */
112
112
  legacyFilesDetected: string[];
113
+ /** PRI-404: Next actions for removing legacy files */
114
+ legacyFileNextActions: string[];
113
115
  }
114
116
 
115
117
  // ─── Helpers ────────────────────────────────────────────────────────────────
@@ -450,6 +452,9 @@ export async function buildDoctorOutput(input: BuildDoctorInput): Promise<Doctor
450
452
  warnings.push(...loadResult.warnings);
451
453
  warnings.push(...flags.warnings);
452
454
 
455
+ // PRI-404: Add legacy file nextActions (separate field, not mixed into general nextActions)
456
+ const { legacyFileNextActions } = loadResult;
457
+
453
458
  if (!loadResult.ok) {
454
459
  for (const err of loadResult.errors) {
455
460
  warnings.push(`Config error at ${err.path}: ${err.reason}`);
@@ -567,6 +572,7 @@ export async function buildDoctorOutput(input: BuildDoctorInput): Promise<Doctor
567
572
  warnings,
568
573
  nextActions: nextActions.length > 0 ? nextActions : ['All checks passed — configuration is valid'],
569
574
  legacyFilesDetected: loadResult.legacyFilesDetected,
575
+ legacyFileNextActions,
570
576
  };
571
577
 
572
578
  if (reason) out.reason = reason;
@@ -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
 
@@ -46,6 +46,8 @@ export interface PdConfigLoadResultOk {
46
46
  warnings: string[];
47
47
  /** If legacy files were detected */
48
48
  legacyFilesDetected: string[];
49
+ /** PRI-404: Next actions for removing legacy files (empty if none detected) */
50
+ legacyFileNextActions: string[];
49
51
  }
50
52
 
51
53
  export interface PdConfigLoadResultErr {
@@ -58,6 +60,8 @@ export interface PdConfigLoadResultErr {
58
60
  /** Warnings from config resolution */
59
61
  warnings: string[];
60
62
  legacyFilesDetected: string[];
63
+ /** PRI-404: Next actions for removing legacy files (empty if none detected) */
64
+ legacyFileNextActions: string[];
61
65
  }
62
66
 
63
67
  export type PdConfigLoadResult = PdConfigLoadResultOk | PdConfigLoadResultErr;
@@ -84,6 +88,13 @@ function detectLegacyFiles(workspaceDir: string): string[] {
84
88
  return detected;
85
89
  }
86
90
 
91
+ /** PRI-404: Generate nextAction suggestions for removing legacy files. */
92
+ function buildLegacyFileNextActions(legacyFiles: string[]): string[] {
93
+ return legacyFiles.map(f => {
94
+ return `Run 'Remove-Item ${f} -Force' to remove legacy config`;
95
+ });
96
+ }
97
+
87
98
  // ── Load PD Config ───────────────────────────────────────────────────────────
88
99
 
89
100
  /**
@@ -98,6 +109,7 @@ function detectLegacyFiles(workspaceDir: string): string[] {
98
109
  export function loadPdConfig(workspaceDir: string): PdConfigLoadResult {
99
110
  const configPath = getPdConfigPath(workspaceDir);
100
111
  const legacyFilesDetected = detectLegacyFiles(workspaceDir);
112
+ const legacyFileNextActions = buildLegacyFileNextActions(legacyFilesDetected);
101
113
 
102
114
  // 1) Config file missing → use defaults
103
115
  if (!fs.existsSync(configPath)) {
@@ -114,6 +126,7 @@ export function loadPdConfig(workspaceDir: string): PdConfigLoadResult {
114
126
  : []),
115
127
  ],
116
128
  legacyFilesDetected,
129
+ legacyFileNextActions,
117
130
  };
118
131
  }
119
132
 
@@ -132,6 +145,7 @@ export function loadPdConfig(workspaceDir: string): PdConfigLoadResult {
132
145
  warnings: [],
133
146
  defaults: effective,
134
147
  legacyFilesDetected,
148
+ legacyFileNextActions,
135
149
  };
136
150
  }
137
151
 
@@ -150,6 +164,7 @@ export function loadPdConfig(workspaceDir: string): PdConfigLoadResult {
150
164
  warnings: [],
151
165
  defaults: effective,
152
166
  legacyFilesDetected,
167
+ legacyFileNextActions,
153
168
  };
154
169
  }
155
170
 
@@ -170,6 +185,7 @@ export function loadPdConfig(workspaceDir: string): PdConfigLoadResult {
170
185
  warnings: [],
171
186
  defaults: effective,
172
187
  legacyFilesDetected,
188
+ legacyFileNextActions,
173
189
  };
174
190
  }
175
191
 
@@ -188,6 +204,7 @@ export function loadPdConfig(workspaceDir: string): PdConfigLoadResult {
188
204
  : []),
189
205
  ],
190
206
  legacyFilesDetected,
207
+ legacyFileNextActions,
191
208
  };
192
209
  }
193
210
 
@@ -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
 
@@ -239,6 +239,21 @@ describe('Legacy file detection', () => {
239
239
  expect(output.legacyFilesDetected[0]).toContain('feature-flags.yaml');
240
240
  } finally { rmTmpDir(tmp); }
241
241
  });
242
+
243
+ it('PRI-404: legacyFileNextActions contains Remove-Item commands and does not pollute nextActions', async () => {
244
+ const tmp = mkTmpDir();
245
+ const stateDir = path.join(tmp, '.state');
246
+ fs.mkdirSync(stateDir, { recursive: true });
247
+ fs.writeFileSync(path.join(stateDir, 'workflows.yaml'), 'version: 1\n', 'utf8');
248
+ try {
249
+ const output = await buildDoctorOutput({ workspaceDir: tmp });
250
+ expect(output.legacyFileNextActions.length).toBeGreaterThan(0);
251
+ expect(output.legacyFileNextActions[0]).toContain('Remove-Item');
252
+ expect(output.legacyFileNextActions[0]).toContain('workflows.yaml');
253
+ // Regression: legacyFileNextActions must NOT appear in general nextActions
254
+ expect(output.nextActions.some(na => na.includes('Remove-Item') && na.includes('workflows.yaml'))).toBe(false);
255
+ } finally { rmTmpDir(tmp); }
256
+ });
242
257
  });
243
258
 
244
259
  // ── Feature flags from config.yaml ───────────────────────────────────────────
@@ -445,6 +445,27 @@ describe('Legacy file detection', () => {
445
445
  expect(result.warnings.some(w => w.includes('Legacy config files detected'))).toBe(true);
446
446
  } finally { rmTmpDir(tmp); }
447
447
  });
448
+
449
+ it('PRI-404: includes legacyFileNextActions with Remove-Item command when legacy files detected', () => {
450
+ const tmp = mkTmpDir();
451
+ const stateDir = path.join(tmp, '.state');
452
+ fs.mkdirSync(stateDir, { recursive: true });
453
+ fs.writeFileSync(path.join(stateDir, 'workflows.yaml'), 'version: 1\n', 'utf8');
454
+ try {
455
+ const result = loadPdConfig(tmp);
456
+ expect(result.legacyFileNextActions.length).toBeGreaterThan(0);
457
+ expect(result.legacyFileNextActions[0]).toContain('Remove-Item');
458
+ expect(result.legacyFileNextActions[0]).toContain('workflows.yaml');
459
+ } finally { rmTmpDir(tmp); }
460
+ });
461
+
462
+ it('PRI-404: legacyFileNextActions is empty when no legacy files', () => {
463
+ const tmp = mkTmpDir();
464
+ try {
465
+ const result = loadPdConfig(tmp);
466
+ expect(result.legacyFileNextActions).toEqual([]);
467
+ } finally { rmTmpDir(tmp); }
468
+ });
448
469
  });
449
470
 
450
471
  // ── JSON purity ──────────────────────────────────────────────────────────────