@principles/pd-cli 1.114.0 → 1.116.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 (50) hide show
  1. package/dist/commands/diagnose.d.ts.map +1 -1
  2. package/dist/commands/diagnose.js +153 -132
  3. package/dist/commands/diagnose.js.map +1 -1
  4. package/dist/commands/runtime-features.d.ts.map +1 -1
  5. package/dist/commands/runtime-features.js +2 -7
  6. package/dist/commands/runtime-features.js.map +1 -1
  7. package/dist/commands/runtime-internalization-integrity-repair.d.ts.map +1 -1
  8. package/dist/commands/runtime-internalization-integrity-repair.js +15 -31
  9. package/dist/commands/runtime-internalization-integrity-repair.js.map +1 -1
  10. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  11. package/dist/commands/runtime-internalization-run-once.js +246 -326
  12. package/dist/commands/runtime-internalization-run-once.js.map +1 -1
  13. package/dist/commands/runtime-recovery.d.ts.map +1 -1
  14. package/dist/commands/runtime-recovery.js +9 -8
  15. package/dist/commands/runtime-recovery.js.map +1 -1
  16. package/dist/services/__tests__/cli-output.test.d.ts +18 -0
  17. package/dist/services/__tests__/cli-output.test.d.ts.map +1 -0
  18. package/dist/services/__tests__/cli-output.test.js +103 -0
  19. package/dist/services/__tests__/cli-output.test.js.map +1 -0
  20. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts +18 -0
  21. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts.map +1 -0
  22. package/dist/services/__tests__/runtime-adapter-resolver.test.js +651 -0
  23. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -0
  24. package/dist/services/cli-output.d.ts +61 -0
  25. package/dist/services/cli-output.d.ts.map +1 -0
  26. package/dist/services/cli-output.js +72 -0
  27. package/dist/services/cli-output.js.map +1 -0
  28. package/dist/services/demo-rule-compiler.d.ts.map +1 -1
  29. package/dist/services/demo-rule-compiler.js +30 -6
  30. package/dist/services/demo-rule-compiler.js.map +1 -1
  31. package/dist/services/runtime-adapter-resolver.d.ts +105 -0
  32. package/dist/services/runtime-adapter-resolver.d.ts.map +1 -0
  33. package/dist/services/runtime-adapter-resolver.js +188 -0
  34. package/dist/services/runtime-adapter-resolver.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/commands/diagnose.ts +146 -138
  37. package/src/commands/runtime-features.ts +2 -6
  38. package/src/commands/runtime-internalization-integrity-repair.ts +16 -28
  39. package/src/commands/runtime-internalization-run-once.ts +242 -353
  40. package/src/commands/runtime-recovery.ts +9 -7
  41. package/src/services/__tests__/cli-output.test.ts +130 -0
  42. package/src/services/__tests__/runtime-adapter-resolver.test.ts +772 -0
  43. package/src/services/cli-output.ts +95 -0
  44. package/src/services/demo-rule-compiler.ts +35 -15
  45. package/src/services/runtime-adapter-resolver.ts +339 -0
  46. package/tests/commands/diagnose.test.ts +7 -3
  47. package/tests/commands/run-rulehost-handler.test.ts +253 -0
  48. package/tests/commands/runtime-internalization-run-once.test.ts +11 -0
  49. package/tests/commands/runtime-recovery.test.ts +27 -4
  50. package/tests/services/demo-rule-compiler.test.ts +242 -0
@@ -23,8 +23,6 @@ import {
23
23
  DisabledDiagnosticianRunner,
24
24
  type DiagnosticianRunnerLike,
25
25
  TestDoubleRuntimeAdapter,
26
- OpenClawCliRuntimeAdapter,
27
- PiAiRuntimeAdapter,
28
26
  PDRuntimeError,
29
27
  isRuntimeConfigError,
30
28
  CandidateIntakeService,
@@ -32,12 +30,12 @@ import {
32
30
  status as diagnoseStatus,
33
31
  PrincipleTreeLedgerAdapter,
34
32
  } from '@principles/core/runtime-v2';
35
- import type { PDRuntimeAdapter, RuntimeConfig, OutputLanguage } from '@principles/core/runtime-v2';
33
+ import type { PDRuntimeAdapter, OutputLanguage } from '@principles/core/runtime-v2';
36
34
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
37
35
  import { readOutputLanguageFromWorkspace } from '../config-reader.js';
38
36
  import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
39
- import { resolveRuntimeFromPdConfig } from '../services/resolve-runtime-from-pd-config.js';
40
37
  import { isFeatureEnabled, SPLIT_PIPELINE_TOTAL_TIMEOUT_MS } from '@principles/core/runtime-v2';
38
+ import { resolveRuntimeAdapterFromConfig, ConfigResolutionError } from '../services/runtime-adapter-resolver.js';
41
39
  import * as path from 'path';
42
40
 
43
41
  function validateStalledThreshold(val: unknown): number | undefined {
@@ -153,6 +151,38 @@ export async function handleDiagnoseStatus(opts: DiagnoseStatusOptions): Promise
153
151
  }
154
152
  }
155
153
 
154
+ /**
155
+ * PRI-431 Step 1d: Build diagnostician-specific test-double payload.
156
+ * Extracted from inline resolution in handleDiagnoseRun to use with shared resolver.
157
+ */
158
+ function buildDiagnosticianTestDouble(taskId: string): PDRuntimeAdapter {
159
+ return new TestDoubleRuntimeAdapter({
160
+ onPollRun: (_runId: string) => ({
161
+ runId: _runId,
162
+ status: 'succeeded',
163
+ startedAt: new Date().toISOString(),
164
+ endedAt: new Date().toISOString(),
165
+ }),
166
+ onFetchOutput: (_runId: string) => ({
167
+ runId: _runId,
168
+ payload: {
169
+ valid: true,
170
+ diagnosisId: `diag-cli-${Date.now()}`,
171
+ taskId,
172
+ summary: 'CLI test diagnosis — validate tool arguments before execution',
173
+ rootCause: 'Test root cause — missing argument validation',
174
+ violatedPrinciples: [],
175
+ evidence: [],
176
+ recommendations: [
177
+ { kind: 'principle', description: 'Always validate tool arguments before execution to prevent silent failures' },
178
+ { kind: 'rule', description: 'Use schema validation for external inputs' },
179
+ ],
180
+ confidence: 0.9,
181
+ },
182
+ }),
183
+ });
184
+ }
185
+
156
186
  /**
157
187
  * pd diagnose run --task-id <taskId> [--workspace <path>] [--json]
158
188
  *
@@ -165,6 +195,7 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
165
195
  if (opts.openclawLocal && opts.openclawGateway) {
166
196
  console.error('error: --openclaw-local and --openclaw-gateway are mutually exclusive');
167
197
  process.exit(1);
198
+ return;
168
199
  }
169
200
 
170
201
  const runtimeKind = opts.runtime ?? 'test-double';
@@ -184,153 +215,130 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
184
215
  const contextAssembler = new SqliteContextAssembler(taskStore, historyQuery, runStore, { sourceTraceLocator });
185
216
 
186
217
  // Select runtime adapter based on --runtime flag (CLI-02)
187
-
218
+ // PRI-431: migrated to shared resolveRuntimeAdapterFromConfig
188
219
  let runtimeAdapter: PDRuntimeAdapter;
189
- if (runtimeKind === 'openclaw-cli') {
190
- const resolved = resolveRuntimeFromPdConfig(workspaceDir);
191
- const configResult = resolved.result;
192
- if (isRuntimeConfigError(configResult)) {
220
+ // Capture config info for telemetry (resolver consumes config internally)
221
+ let telemetryConfig: { provider?: string; model?: string; openclawMode?: string } = {};
222
+ try {
223
+ if (runtimeKind === 'test-double') {
224
+ runtimeAdapter = resolveRuntimeAdapterFromConfig({
225
+ runtimeKind,
226
+ workspaceDir,
227
+ allowTestDouble: true,
228
+ testDoublePayloadBuilder: () => buildDiagnosticianTestDouble(opts.taskId),
229
+ });
230
+ } else if (runtimeKind === 'openclaw-cli') {
231
+ const flagMode: 'local' | 'gateway' | undefined = opts.openclawLocal
232
+ ? 'local'
233
+ : opts.openclawGateway
234
+ ? 'gateway'
235
+ : undefined;
236
+ runtimeAdapter = resolveRuntimeAdapterFromConfig({
237
+ runtimeKind,
238
+ workspaceDir,
239
+ openclawMode: flagMode,
240
+ agentId: opts.agent ?? 'main',
241
+ onConfigResolved: (resolved) => {
242
+ if (!isRuntimeConfigError(resolved.result)) {
243
+ telemetryConfig.openclawMode = flagMode ?? resolved.result.openclawMode;
244
+ } else {
245
+ telemetryConfig.openclawMode = flagMode;
246
+ }
247
+ },
248
+ });
249
+ // TELE-01: runtime_adapter_selected — user explicitly chose openclaw-cli runtime
250
+ storeEmitter.emitTelemetry({
251
+ eventType: 'runtime_adapter_selected',
252
+ traceId: opts.taskId,
253
+ timestamp: new Date().toISOString(),
254
+ sessionId: 'pd-cli-diagnose',
255
+ agentId: 'openclaw-cli-adapter',
256
+ payload: {
257
+ runtimeKind: 'openclaw-cli',
258
+ runtimeMode: telemetryConfig.openclawMode ?? 'local',
259
+ },
260
+ });
261
+ } else if (runtimeKind === 'pi-ai') {
262
+ runtimeAdapter = resolveRuntimeAdapterFromConfig({
263
+ runtimeKind,
264
+ workspaceDir,
265
+ configOptional: true,
266
+ validateApiKeyEnv: true,
267
+ piAiOverrides: {
268
+ provider: opts.provider,
269
+ model: opts.model,
270
+ apiKeyEnv: opts.apiKeyEnv,
271
+ baseUrl: opts.baseUrl,
272
+ maxRetries: opts.maxRetries,
273
+ },
274
+ timeoutMs: opts.timeoutMs,
275
+ onConfigResolved: (resolved) => {
276
+ for (const w of resolved.legacyWarnings) console.warn(`[pd diagnose] ${w}`);
277
+ if (isRuntimeConfigError(resolved.result)) {
278
+ console.warn(`[pd diagnose] .pd/config.yaml resolution failed: ${resolved.result.message}. Using CLI flags if provided.`);
279
+ } else {
280
+ telemetryConfig.provider = resolved.result.provider;
281
+ telemetryConfig.model = resolved.result.model;
282
+ }
283
+ },
284
+ });
285
+ // TELE: runtime_adapter_selected telemetry
286
+ const telemetryProvider = opts.provider ?? telemetryConfig.provider;
287
+ const telemetryModel = opts.model ?? telemetryConfig.model;
288
+ storeEmitter.emitTelemetry({
289
+ eventType: 'runtime_adapter_selected',
290
+ traceId: opts.taskId,
291
+ timestamp: new Date().toISOString(),
292
+ sessionId: 'pd-cli-diagnose',
293
+ agentId: 'pi-ai-adapter',
294
+ payload: { runtimeKind: 'pi-ai', provider: telemetryProvider, model: telemetryModel, baseUrlPresent: !!opts.baseUrl },
295
+ });
296
+ } else {
297
+ const unsupportedResult = {
298
+ ok: false,
299
+ reason: `unsupported_runtime_kind: ${runtimeKind}`,
300
+ nextAction: 'Use one of: openclaw-cli, test-double, pi-ai',
301
+ };
193
302
  if (opts.json) {
194
- console.log(JSON.stringify({ ok: false, reason: configResult.reason, message: configResult.message, nextAction: configResult.nextAction }));
303
+ console.log(JSON.stringify(unsupportedResult, null, 2));
195
304
  } else {
196
- console.error(`error: ${configResult.message}`);
197
- console.error(`nextAction: ${configResult.nextAction}`);
305
+ console.error(`error: unknown runtime kind '${runtimeKind}' (supported: openclaw-cli, test-double, pi-ai)`);
306
+ console.error(`Next action: ${unsupportedResult.nextAction}`);
198
307
  }
199
308
  process.exit(1);
200
309
  return;
201
310
  }
202
- const { openclawMode } = configResult;
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) {
311
+ } catch (err) {
312
+ if (err instanceof ConfigResolutionError) {
313
+ // Preserve original error format for openclaw-cli missing mode (backward compat)
314
+ if (err.kind === 'missing-fields' && err.missing?.includes('openclawMode')) {
315
+ if (opts.json) {
316
+ console.log(JSON.stringify({
317
+ ok: false,
318
+ reason: 'missing_openclaw_mode',
319
+ message: 'runtimeKind is openclaw-cli but no mode resolved',
320
+ nextAction: 'Provide --openclaw-local or --openclaw-gateway, or set openclawMode in .pd/config.yaml',
321
+ }));
322
+ } else {
323
+ console.error('error: runtimeKind is openclaw-cli but no mode resolved');
324
+ console.error('nextAction: Provide --openclaw-local or --openclaw-gateway, or set openclawMode in .pd/config.yaml');
325
+ }
326
+ process.exit(1);
327
+ return;
328
+ }
329
+ // Default error formatting for other config resolution errors
207
330
  if (opts.json) {
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' }));
331
+ console.log(JSON.stringify({ ok: false, reason: err.kind, message: err.message, missing: err.missing, nextAction: err.nextAction ?? 'Check .pd/config.yaml and retry' }));
209
332
  } else {
210
- console.error('error: runtimeKind is openclaw-cli but no mode resolved');
211
- console.error('nextAction: Provide --openclaw-local or --openclaw-gateway, or set openclawMode in .pd/config.yaml');
333
+ console.error(`error: ${err.message}`);
334
+ if (err.nextAction) {
335
+ console.error(`nextAction: ${err.nextAction}`);
336
+ }
212
337
  }
213
338
  process.exit(1);
214
339
  return;
215
340
  }
216
-
217
- runtimeAdapter = new OpenClawCliRuntimeAdapter({
218
- runtimeMode: effectiveMode,
219
- workspaceDir,
220
- agentId: opts.agent ?? 'main',
221
- });
222
-
223
- // TELE-01: runtime_adapter_selected — user explicitly chose openclaw-cli runtime
224
- storeEmitter.emitTelemetry({
225
- eventType: 'runtime_adapter_selected',
226
- traceId: opts.taskId,
227
- timestamp: new Date().toISOString(),
228
- sessionId: 'pd-cli-diagnose',
229
- agentId: 'openclaw-cli-adapter',
230
- payload: {
231
- runtimeKind: 'openclaw-cli',
232
- runtimeMode: effectiveMode,
233
- },
234
- });
235
- } else if (runtimeKind === 'test-double') {
236
- runtimeAdapter = new TestDoubleRuntimeAdapter({
237
- onPollRun: (_runId: string) => ({
238
- runId: _runId,
239
- status: 'succeeded',
240
- startedAt: new Date().toISOString(),
241
- endedAt: new Date().toISOString(),
242
- }),
243
- onFetchOutput: (_runId: string) => ({
244
- runId: _runId,
245
- payload: {
246
- valid: true,
247
- diagnosisId: `diag-cli-${Date.now()}`,
248
- taskId: opts.taskId,
249
- summary: 'CLI test diagnosis — validate tool arguments before execution',
250
- rootCause: 'Test root cause — missing argument validation',
251
- violatedPrinciples: [],
252
- evidence: [],
253
- recommendations: [
254
- { kind: 'principle', description: 'Always validate tool arguments before execution to prevent silent failures' },
255
- { kind: 'rule', description: 'Use schema validation for external inputs' },
256
- ],
257
- confidence: 0.9,
258
- },
259
- }),
260
- });
261
- } else if (runtimeKind === 'pi-ai') {
262
- const resolved = resolveRuntimeFromPdConfig(workspaceDir);
263
- for (const w of resolved.legacyWarnings) console.warn(`[pd diagnose] ${w}`);
264
-
265
- let policyConfig: RuntimeConfig | null = null;
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.`);
270
- }
271
-
272
- const provider = opts.provider ?? policyConfig?.provider;
273
- const model = opts.model ?? policyConfig?.model;
274
- const apiKeyEnv = opts.apiKeyEnv ?? policyConfig?.apiKeyEnv;
275
- const baseUrl = opts.baseUrl ?? policyConfig?.baseUrl;
276
- const maxRetries = opts.maxRetries ?? policyConfig?.maxRetries;
277
- const effectiveTimeoutMs = opts.timeoutMs ?? policyConfig?.timeoutMs;
278
-
279
- // D-11: validate config — missing fields + fix suggestion
280
- const missing: string[] = [];
281
- if (!provider) missing.push('provider');
282
- if (!model) missing.push('model');
283
- if (!apiKeyEnv) missing.push('apiKeyEnv');
284
- if (missing.length > 0) {
285
- console.error(
286
- `error: missing required pi-ai config: ${missing.join(', ')}.\n` +
287
- `Pass via --flag or add to .pd/config.yaml runtime profile.\n` +
288
- `Example:\n` +
289
- ` pd diagnose run --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY\n` +
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
- );
298
- process.exit(1);
299
- }
300
-
301
- // After validation: all fields are confirmed non-null
302
- const validProvider: string = provider as string;
303
- const validModel: string = model as string;
304
- const validApiKeyEnv: string = apiKeyEnv as string;
305
-
306
- // D-09: validate env var exists
307
- if (!process.env[validApiKeyEnv]) {
308
- console.error(`error: environment variable '${validApiKeyEnv}' is not set`);
309
- process.exit(1);
310
- }
311
-
312
- runtimeAdapter = new PiAiRuntimeAdapter({
313
- provider: validProvider,
314
- model: validModel,
315
- apiKeyEnv: validApiKeyEnv,
316
- baseUrl,
317
- maxRetries,
318
- timeoutMs: effectiveTimeoutMs,
319
- workspace: workspaceDir,
320
- });
321
-
322
- // TELE: runtime_adapter_selected telemetry
323
- storeEmitter.emitTelemetry({
324
- eventType: 'runtime_adapter_selected',
325
- traceId: opts.taskId,
326
- timestamp: new Date().toISOString(),
327
- sessionId: 'pd-cli-diagnose',
328
- agentId: 'pi-ai-adapter',
329
- payload: { runtimeKind: 'pi-ai', provider: validProvider, model: validModel, baseUrlPresent: !!baseUrl },
330
- });
331
- } else {
332
- console.error(`error: unknown runtime kind '${runtimeKind}' (supported: openclaw-cli, test-double, pi-ai)`);
333
- process.exit(1);
341
+ throw err;
334
342
  }
335
343
 
336
344
  const eventEmitter = new StoreEventEmitter();
@@ -12,6 +12,7 @@
12
12
  import * as path from 'path';
13
13
  import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
14
14
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
15
+ import { emitResult } from '../services/cli-output.js';
15
16
 
16
17
  // ── Output types ─────────────────────────────────────────────────────────────
17
18
 
@@ -144,12 +145,7 @@ export async function handleRuntimeFeaturesStatus(opts: FeaturesOptions): Promis
144
145
 
145
146
  const output = buildRuntimeFeaturesStatus(workspaceDir);
146
147
 
147
- if (opts.json) {
148
- // JSON mode: single parseable object on stdout
149
- console.log(JSON.stringify(output, null, 2));
150
- } else {
151
- console.log(formatTextOutput(output));
152
- }
148
+ emitResult(output, { json: opts.json ?? false, formatText: formatTextOutput });
153
149
 
154
150
  if (output.status === 'failed') {
155
151
  process.exitCode = 1;
@@ -2,6 +2,7 @@ import * as path from 'path';
2
2
  import { InternalizationIntegrityRemediation } from '@principles/core/runtime-v2';
3
3
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
4
4
  import type { RemediationResult } from './remediation-output.js';
5
+ import { emitResult, emitFlagConflict, emitError } from '../services/cli-output.js';
5
6
 
6
7
  interface InternalizationIntegrityRepairOptions {
7
8
  workspace?: string;
@@ -43,16 +44,8 @@ function formatTextOutput(result: RemediationResult): string {
43
44
 
44
45
  export async function handleRuntimeInternalizationIntegrityRepair(opts: InternalizationIntegrityRepairOptions): Promise<void> {
45
46
  if (opts.dryRun && opts.confirm) {
46
- if (opts.json) {
47
- console.log(JSON.stringify({
48
- ok: false,
49
- reason: 'Error: --dry-run and --confirm are mutually exclusive',
50
- nextAction: 'Specify only one of --dry-run or --confirm',
51
- }, null, 2));
52
- } else {
53
- console.error('Error: --dry-run and --confirm are mutually exclusive. Specify one or the other.');
54
- }
55
- process.exit(1);
47
+ const exitCode = emitFlagConflict({ json: opts.json ?? false });
48
+ process.exit(exitCode);
56
49
  return;
57
50
  }
58
51
 
@@ -66,26 +59,21 @@ export async function handleRuntimeInternalizationIntegrityRepair(opts: Internal
66
59
  const remediation = new InternalizationIntegrityRemediation({ workspaceDir });
67
60
  const result = remediation.repair({ dryRun: isDryRun });
68
61
 
69
- if (opts.json) {
70
- console.log(JSON.stringify(result, null, 2));
71
- } else {
72
- console.log(formatTextOutput(result));
73
- if (!isDryRun && result.repairedCount === 0 && result.actions.length > 0) {
74
- console.error('');
75
- console.error('NOTE: No repairs were made. All issues were already resolved or skipped.');
76
- }
62
+ emitResult(result, {
63
+ json: opts.json ?? false,
64
+ formatText: formatTextOutput,
65
+ });
66
+
67
+ if (!isDryRun && result.repairedCount === 0 && result.actions.length > 0 && !opts.json) {
68
+ console.error('');
69
+ console.error('NOTE: No repairs were made. All issues were already resolved or skipped.');
77
70
  }
78
71
  } catch (err: unknown) {
79
- if (opts.json) {
80
- console.log(JSON.stringify({
81
- ok: false,
82
- reason: err instanceof Error ? err.message : String(err),
83
- nextAction: 'Check workspace path and DB connectivity',
84
- }, null, 2));
85
- } else {
86
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
87
- }
88
- process.exit(1);
72
+ const exitCode = emitError(err, {
73
+ json: opts.json ?? false,
74
+ nextAction: 'Check workspace path and DB connectivity',
75
+ });
76
+ process.exit(exitCode);
89
77
  return;
90
78
  }
91
79
  }