@principles/pd-cli 1.73.1 → 1.74.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.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * pd config doctor — Discover and explain PD / OpenClaw configuration state.
3
+ *
4
+ * PRI-299 MVP UX:
5
+ * - Reports workspace + OpenClaw config paths and existence
6
+ * - Lists effective feature flags and enabled MVP channels
7
+ * - Classifies provider/model/auth connectivity (healthy, auth_missing, rate_limit, etc.)
8
+ * - Emits `reason` + `nextActions` for failures
9
+ * - NEVER leaks tokens, env var values, or raw config bytes
10
+ *
11
+ * Usage:
12
+ * pd config doctor [--workspace <path>] [--json]
13
+ */
14
+
15
+ import * as path from 'path';
16
+ import { resolveWorkspaceDir } from '../resolve-workspace.js';
17
+ import { buildDoctorOutput, type DoctorOutput } from '../services/config-doctor.js';
18
+
19
+ interface DoctorOptions {
20
+ workspace?: string;
21
+ json?: boolean;
22
+ }
23
+
24
+ function formatTextOutput(output: DoctorOutput): string {
25
+ const lines: string[] = [];
26
+ const statusIcon = output.status === 'ok' ? '✓' : output.status === 'degraded' ? '⚠' : '✗';
27
+
28
+ lines.push('PD Config Doctor');
29
+ lines.push(`status: ${statusIcon} ${output.status.toUpperCase()}`);
30
+ lines.push(`workspace: ${output.workspaceDir}`);
31
+ lines.push('');
32
+
33
+ lines.push('PD config paths:');
34
+ for (const [k, v] of Object.entries(output.pdConfigPaths)) {
35
+ const exists = v.exists ? '[exists]' : '[missing]';
36
+ lines.push(` ${k.padEnd(16)} ${exists.padEnd(10)} ${v.path}`);
37
+ }
38
+ lines.push('');
39
+
40
+ lines.push('OpenClaw paths:');
41
+ for (const [k, v] of Object.entries(output.openclawConfigPaths)) {
42
+ const exists = v.exists ? '[exists]' : '[missing]';
43
+ lines.push(` ${k.padEnd(16)} ${exists.padEnd(10)} ${v.path}`);
44
+ }
45
+ lines.push('');
46
+
47
+ lines.push(`Feature flags: source=${output.featureFlags.source}`);
48
+ lines.push(` enabled MVP channels: ${output.featureFlags.enabledMvpChannels.length === 0 ? '(none)' : output.featureFlags.enabledMvpChannels.join(', ')}`);
49
+ if (output.featureFlags.disabledFlags.length > 0) {
50
+ lines.push(` disabled: ${output.featureFlags.disabledFlags.join(', ')}`);
51
+ }
52
+ if (output.featureFlags.warnings.length > 0) {
53
+ lines.push(` warnings: ${output.featureFlags.warnings.length}`);
54
+ }
55
+ lines.push('');
56
+
57
+ lines.push('Provider health:');
58
+ if (output.providerHealth.length === 0) {
59
+ lines.push(' (no providers discovered)');
60
+ } else {
61
+ for (const p of output.providerHealth) {
62
+ const cls = p.classification.toUpperCase();
63
+ const provider = p.provider ?? '(unset)';
64
+ const model = p.model ?? '(unset)';
65
+ const apiKeyEnv = p.apiKeyEnv ?? '(unset)';
66
+ const apiKeyState = p.apiKeyPresent ? 'present' : 'absent';
67
+ lines.push(` [${cls}] ${provider} / ${model}`);
68
+ lines.push(` apiKeyEnv: ${apiKeyEnv} (${apiKeyState})`);
69
+ lines.push(` source: ${p.source}`);
70
+ lines.push(` reason: ${p.reason}`);
71
+ lines.push(` nextAction: ${p.nextAction}`);
72
+ }
73
+ }
74
+ lines.push('');
75
+
76
+ lines.push('Internal agents:');
77
+ if (output.internalAgents && output.internalAgents.correctionObserver) {
78
+ const co = output.internalAgents.correctionObserver;
79
+ const coStatus = co.status.toUpperCase();
80
+ const coProvider = co.provider ?? '(unset)';
81
+ const coModel = co.model ?? '(unset)';
82
+ const coApiKeyEnv = co.apiKeyEnv ?? '(unset)';
83
+ const coApiKeyState = co.apiKeyPresent ? 'present' : 'absent';
84
+ lines.push(` correctionObserver: [${coStatus}]`);
85
+ lines.push(` enabled: ${co.enabled}`);
86
+ lines.push(` flagSource: ${co.flagSource}`);
87
+ lines.push(` configSource:${co.configSource}`);
88
+ if (co.provider || co.model) {
89
+ lines.push(` provider: ${coProvider} / ${coModel}`);
90
+ }
91
+ lines.push(` apiKeyEnv: ${coApiKeyEnv} (${coApiKeyState})`);
92
+ lines.push(` reason: ${co.reason}`);
93
+ lines.push(` nextAction: ${co.nextAction}`);
94
+ } else {
95
+ lines.push(' (no internal agents diagnosed)');
96
+ }
97
+ lines.push('');
98
+
99
+ if (output.warnings.length > 0) {
100
+ lines.push('Warnings:');
101
+ for (const w of output.warnings) {
102
+ lines.push(` [!] ${w}`);
103
+ }
104
+ lines.push('');
105
+ }
106
+
107
+ if (output.reason) {
108
+ lines.push(`Reason: ${output.reason}`);
109
+ lines.push('');
110
+ }
111
+
112
+ if (output.nextActions.length > 0) {
113
+ lines.push('Next actions:');
114
+ for (const a of output.nextActions) {
115
+ lines.push(` → ${a}`);
116
+ }
117
+ }
118
+
119
+ return lines.join('\n');
120
+ }
121
+
122
+ export async function handleConfigDoctor(opts: DoctorOptions): Promise<void> {
123
+ let workspaceDir: string;
124
+ try {
125
+ workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
126
+ } catch (err) {
127
+ const message = err instanceof Error ? err.message : String(err);
128
+ const output = {
129
+ status: 'failed' as const,
130
+ reason: 'workspace_resolution_failed',
131
+ message,
132
+ nextActions: ['Pass --workspace <path> explicitly, or set PD_WORKSPACE_DIR environment variable'],
133
+ };
134
+ if (opts.json) {
135
+ console.log(JSON.stringify(output, null, 2));
136
+ } else {
137
+ console.error(`error: ${message}`);
138
+ }
139
+ process.exit(1);
140
+ return;
141
+ }
142
+
143
+ const output = await buildDoctorOutput({ workspaceDir });
144
+
145
+ if (opts.json) {
146
+ // JSON mode: single parseable object on stdout.
147
+ console.log(JSON.stringify(output, null, 2));
148
+ } else {
149
+ console.log(formatTextOutput(output));
150
+ }
151
+
152
+ if (output.status === 'failed') {
153
+ process.exitCode = 1;
154
+ } else if (output.status === 'degraded') {
155
+ // degraded: do not exit 1, but emit a hint to stderr for the operator.
156
+ console.error(`\nNote: doctor status is degraded. Inspect warnings + nextActions.`);
157
+ }
158
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /**
3
- * pd CLI Principles Disciple command-line interface.
3
+ * pd CLI 鈥?Principles Disciple command-line interface.
4
4
  *
5
5
  * Usage:
6
6
  * pd pain record --reason <text> [--score N] [--source manual]
@@ -47,12 +47,16 @@ import { handleProvenChannelBaseline } from './commands/proven-channel-baseline.
47
47
  import { handleDemoStoryA } from './commands/demo-story-a.js';
48
48
  import { handleRuntimeFeaturesStatus } from './commands/runtime-features.js';
49
49
 
50
+ import { createRequire } from 'module';
51
+ const require = createRequire(import.meta.url);
52
+ const pkg = require('../package.json') as { version: string };
53
+
50
54
  const program = new Command();
51
55
 
52
56
  program
53
57
  .name('pd')
54
- .description('PD CLI Pain recording, sample management, and evolution tasks')
55
- .version('0.1.0');
58
+ .description('PD CLI 鈥?Pain recording, sample management, and evolution tasks')
59
+ .version(pkg.version);
56
60
 
57
61
  const painCmd = program
58
62
  .command('pain')
@@ -143,7 +147,7 @@ centralCmd
143
147
  await handleCentralSync();
144
148
  });
145
149
 
146
- // ── Runtime v2 task/run commands ──────────────────────────────────────────────
150
+ // 鈹€鈹€ Runtime v2 task/run commands 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
147
151
 
148
152
  const rtTaskCmd = program
149
153
  .command('task')
@@ -186,7 +190,7 @@ rtRunCmd
186
190
  await handleRunShow({ id: runId });
187
191
  });
188
192
 
189
- // ── Runtime v2 trajectory/history/context commands ───────────────────────────
193
+ // 鈹€鈹€ Runtime v2 trajectory/history/context commands 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
190
194
 
191
195
  const trajectoryCmd = program
192
196
  .command('trajectory')
@@ -235,7 +239,7 @@ contextCmd
235
239
  await handleContextBuild(taskId, opts);
236
240
  });
237
241
 
238
- // ── Legacy import command ───────────────────────────────────────────────────────
242
+ // 鈹€鈹€ Legacy import command 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
239
243
 
240
244
  const legacyCmd = program
241
245
  .command('legacy')
@@ -254,7 +258,7 @@ importCmd
254
258
  await handleLegacyImportOpenClaw(opts);
255
259
  });
256
260
 
257
- // ── Diagnostician run/status commands ─────────────────────────────────────
261
+ // 鈹€鈹€ Diagnostician run/status commands 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
258
262
 
259
263
  const diagnoseCmd = program
260
264
  .command('diagnose')
@@ -279,19 +283,19 @@ diagnoseCmd
279
283
  .option('--openclaw-local', 'Use local OpenClaw (mutually exclusive with --openclaw-gateway)')
280
284
  .option('--openclaw-gateway', 'Use gateway OpenClaw (mutually exclusive with --openclaw-local)')
281
285
  .option('-a, --agent <agentId>', 'Agent ID to invoke')
282
- .option('--provider <name>', 'LLM provider (e.g., openrouter) for pi-ai, falls back to policy')
283
- .option('--model <id>', 'Model ID (e.g., anthropic/claude-sonnet-4) for pi-ai, falls back to policy')
284
- .option('--apiKeyEnv <name>', 'Env var name for API key for pi-ai, falls back to policy')
285
- .option('--baseUrl <url>', 'Custom base URL for pi-ai, falls back to policy')
286
- .option('--maxRetries <n>', 'Max retry attempts for LLM failures for pi-ai, falls back to policy', parseInt)
287
- .option('--timeoutMs <ms>', 'Timeout in milliseconds for pi-ai, falls back to policy', parseInt)
286
+ .option('--provider <name>', 'LLM provider (e.g., openrouter) 鈥?for pi-ai, falls back to policy')
287
+ .option('--model <id>', 'Model ID (e.g., anthropic/claude-sonnet-4) 鈥?for pi-ai, falls back to policy')
288
+ .option('--apiKeyEnv <name>', 'Env var name for API key 鈥?for pi-ai, falls back to policy')
289
+ .option('--baseUrl <url>', 'Custom base URL 鈥?for pi-ai, falls back to policy')
290
+ .option('--maxRetries <n>', 'Max retry attempts for LLM failures 鈥?for pi-ai, falls back to policy', parseInt)
291
+ .option('--timeoutMs <ms>', 'Timeout in milliseconds 鈥?for pi-ai, falls back to policy', parseInt)
288
292
  .option('--no-intake', 'Skip candidate intake after successful diagnosis')
289
293
  .option('--json', 'Output raw JSON')
290
294
  .action(async (opts) => {
291
295
  await handleDiagnoseRun(opts);
292
296
  });
293
297
 
294
- // ── Runtime probe command (HG-01 HARD GATE) ─────────────────────────────────
298
+ // 鈹€鈹€ Runtime probe command (HG-01 HARD GATE) 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
295
299
 
296
300
  const runtimeCmd = program
297
301
  .command('runtime')
@@ -312,7 +316,7 @@ const synthCmd = runtimeCmd
312
316
 
313
317
  synthCmd
314
318
  .command('baseline')
315
- .description('Run synthetic PD workload baseline (PRI-206) deterministic, no LLM required')
319
+ .description('Run synthetic PD workload baseline (PRI-206) 鈥?deterministic, no LLM required')
316
320
  .option('-w, --workspace <path>', 'Workspace directory (default: temp workspace)')
317
321
  .option('--json', 'Output raw JSON')
318
322
  .action(async (opts) => {
@@ -321,7 +325,7 @@ synthCmd
321
325
 
322
326
  synthCmd
323
327
  .command('flood')
324
- .description('Run pain flood simulation (PRI-208) deterministic dedup/stress test, no LLM required')
328
+ .description('Run pain flood simulation (PRI-208) 鈥?deterministic dedup/stress test, no LLM required')
325
329
  .option('-w, --workspace <path>', 'Workspace directory (default: temp workspace)')
326
330
  .option('--json', 'Output raw JSON')
327
331
  .option('--identical-count <n>', 'Number of identical pain signals (default: 10)', parseInt)
@@ -339,7 +343,7 @@ synthCmd
339
343
 
340
344
  synthCmd
341
345
  .command('proven-channel')
342
- .description('Run MVP activation continuity baseline (PRI-240) deterministic, no LLM required')
346
+ .description('Run MVP activation continuity baseline (PRI-240) 鈥?deterministic, no LLM required')
343
347
  .option('-w, --workspace <path>', 'Workspace directory (default: temp workspace)')
344
348
  .option('--json', 'Output raw JSON')
345
349
  .option('--channels <channels>', 'Comma-separated channel list (prompt,code_tool_hook,defer_archive)')
@@ -369,7 +373,7 @@ const demoCmd = program
369
373
 
370
374
  demoCmd
371
375
  .command('story-a')
372
- .description('Run Story A\' proven-channel demo (PRI-246) full evidence→proposal→approval→activation→observation chain')
376
+ .description('Run Story A\' proven-channel demo (PRI-246) 鈥?full evidence鈫抪roposal鈫抋pproval鈫抋ctivation鈫抩bservation chain')
373
377
  .option('-w, --workspace <path>', 'Workspace directory (default: temp workspace)')
374
378
  .option('--json', 'Output raw JSON')
375
379
  .option('--channels <channels>', 'Comma-separated channel list (prompt,code_tool_hook,defer_archive)')
@@ -388,13 +392,13 @@ runtimeCmd
388
392
  .option('--openclaw-local', 'Use local OpenClaw (mutually exclusive with --openclaw-gateway)')
389
393
  .option('--openclaw-gateway', 'Use gateway OpenClaw (mutually exclusive with --openclaw-local)')
390
394
  .option('-a, --agent <agentId>', 'Agent ID to probe')
391
- .option('--provider <name>', 'LLM provider (e.g., openrouter) for pi-ai, falls back to --workspace workflows.yaml')
392
- .option('--model <id>', 'Model ID (e.g., anthropic/claude-sonnet-4) for pi-ai, falls back to --workspace workflows.yaml')
393
- .option('--apiKeyEnv <name>', 'Env var name for API key (e.g., OPENROUTER_API_KEY) for pi-ai, falls back to --workspace workflows.yaml')
394
- .option('--baseUrl <url>', 'Custom base URL for OpenAI-compatible providers for pi-ai, falls back to --workspace workflows.yaml')
395
+ .option('--provider <name>', 'LLM provider (e.g., openrouter) 鈥?for pi-ai, falls back to --workspace workflows.yaml')
396
+ .option('--model <id>', 'Model ID (e.g., anthropic/claude-sonnet-4) 鈥?for pi-ai, falls back to --workspace workflows.yaml')
397
+ .option('--apiKeyEnv <name>', 'Env var name for API key (e.g., OPENROUTER_API_KEY) 鈥?for pi-ai, falls back to --workspace workflows.yaml')
398
+ .option('--baseUrl <url>', 'Custom base URL for OpenAI-compatible providers 鈥?for pi-ai, falls back to --workspace workflows.yaml')
395
399
  .option('--maxRetries <n>', 'Max retry attempts for LLM failures', parseInt)
396
400
  .option('--timeoutMs <ms>', 'Timeout in milliseconds for probe', parseInt)
397
- .option('-w, --workspace <path>', 'Workspace directory loads pi-ai policy from .state/workflows.yaml')
401
+ .option('-w, --workspace <path>', 'Workspace directory 鈥?loads pi-ai policy from .state/workflows.yaml')
398
402
  .option('--json', 'Output raw JSON')
399
403
  .action(async (opts) => {
400
404
  await handleRuntimeProbe(opts);
@@ -458,17 +462,17 @@ runtimeHealthCmd
458
462
 
459
463
  runtimeHealthCmd
460
464
  .command('gfi')
461
- .description('GFI workspace snapshot active vs stale session breakdown')
465
+ .description('GFI workspace snapshot 鈥?active vs stale session breakdown')
462
466
  .option('-w, --workspace <path>', 'Workspace directory')
463
467
  .option('--json', 'Output raw JSON')
464
468
  .action(async (opts) => {
465
469
  await handleRuntimeGfiSnapshot({ workspace: opts.workspace, json: opts.json });
466
470
  });
467
471
 
468
- // PRI-82: pd runtime gfi snapshot canonical operator command (alias of runtime health gfi)
472
+ // PRI-82: pd runtime gfi snapshot 鈥?canonical operator command (alias of runtime health gfi)
469
473
  runtimeCmd
470
474
  .command('gfi')
471
- .description('GFI workspace snapshot active vs stale session breakdown')
475
+ .description('GFI workspace snapshot 鈥?active vs stale session breakdown')
472
476
  .command('snapshot')
473
477
  .description('GFI workspace snapshot for the operator (alias: pd runtime health gfi)')
474
478
  .option('-w, --workspace <path>', 'Workspace directory')
@@ -604,7 +608,7 @@ const pruningCmd = runtimeCmd
604
608
 
605
609
  pruningCmd
606
610
  .command('report')
607
- .description('Show pruning health report watch/review principle signals')
611
+ .description('Show pruning health report 鈥?watch/review principle signals')
608
612
  .option('-w, --workspace <path>', 'Workspace directory')
609
613
  .option('--json', 'Output raw JSON')
610
614
  .action((opts) => {
@@ -675,7 +679,7 @@ pruningCmd
675
679
  });
676
680
  });
677
681
 
678
- // ── Candidate inspection commands ───────────────────────────────────────────
682
+ // 鈹€鈹€ Candidate inspection commands 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
679
683
 
680
684
  const candidateCmd = program
681
685
  .command('candidate')
@@ -779,7 +783,7 @@ candidateInternalizationCmd
779
783
  await handleCandidateInternalizationBackfill({ workspace: opts.workspace, dryRun: opts.dryRun, confirm: opts.confirm, includePending: opts.includePending, json: opts.json });
780
784
  });
781
785
 
782
- // ── Artifact inspection commands ────────────────────────────────────────────
786
+ // 鈹€鈹€ Artifact inspection commands 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
783
787
 
784
788
  const artifactCmd = program
785
789
  .command('artifact')
@@ -826,4 +830,22 @@ program
826
830
  });
827
831
  });
828
832
 
833
+ // PRI-299: PD Config Doctor 鈥?surface config paths, feature flags, and provider connectivity
834
+ const configCmd = program
835
+ .command('config')
836
+ .description('PD config diagnostics');
837
+
838
+ configCmd
839
+ .command('doctor')
840
+ .description('Inspect PD / OpenClaw config paths, feature flags, and provider connectivity (PRI-299)')
841
+ .option('-w, --workspace <path>', 'Workspace directory')
842
+ .option('--json', 'Output a single parseable JSON object on stdout', false)
843
+ .action(async (opts) => {
844
+ const { handleConfigDoctor } = await import('./commands/config-doctor.js');
845
+ await handleConfigDoctor({
846
+ workspace: opts.workspace,
847
+ json: opts.json,
848
+ });
849
+ });
850
+
829
851
  program.parse();