@principles/pd-cli 1.73.2 → 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]
@@ -55,7 +55,7 @@ const program = new Command();
55
55
 
56
56
  program
57
57
  .name('pd')
58
- .description('PD CLI Pain recording, sample management, and evolution tasks')
58
+ .description('PD CLI 鈥?Pain recording, sample management, and evolution tasks')
59
59
  .version(pkg.version);
60
60
 
61
61
  const painCmd = program
@@ -147,7 +147,7 @@ centralCmd
147
147
  await handleCentralSync();
148
148
  });
149
149
 
150
- // ── Runtime v2 task/run commands ──────────────────────────────────────────────
150
+ // 鈹€鈹€ Runtime v2 task/run commands 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
151
151
 
152
152
  const rtTaskCmd = program
153
153
  .command('task')
@@ -190,7 +190,7 @@ rtRunCmd
190
190
  await handleRunShow({ id: runId });
191
191
  });
192
192
 
193
- // ── Runtime v2 trajectory/history/context commands ───────────────────────────
193
+ // 鈹€鈹€ Runtime v2 trajectory/history/context commands 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
194
194
 
195
195
  const trajectoryCmd = program
196
196
  .command('trajectory')
@@ -239,7 +239,7 @@ contextCmd
239
239
  await handleContextBuild(taskId, opts);
240
240
  });
241
241
 
242
- // ── Legacy import command ───────────────────────────────────────────────────────
242
+ // 鈹€鈹€ Legacy import command 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
243
243
 
244
244
  const legacyCmd = program
245
245
  .command('legacy')
@@ -258,7 +258,7 @@ importCmd
258
258
  await handleLegacyImportOpenClaw(opts);
259
259
  });
260
260
 
261
- // ── Diagnostician run/status commands ─────────────────────────────────────
261
+ // 鈹€鈹€ Diagnostician run/status commands 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
262
262
 
263
263
  const diagnoseCmd = program
264
264
  .command('diagnose')
@@ -283,19 +283,19 @@ diagnoseCmd
283
283
  .option('--openclaw-local', 'Use local OpenClaw (mutually exclusive with --openclaw-gateway)')
284
284
  .option('--openclaw-gateway', 'Use gateway OpenClaw (mutually exclusive with --openclaw-local)')
285
285
  .option('-a, --agent <agentId>', 'Agent ID to invoke')
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)
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)
292
292
  .option('--no-intake', 'Skip candidate intake after successful diagnosis')
293
293
  .option('--json', 'Output raw JSON')
294
294
  .action(async (opts) => {
295
295
  await handleDiagnoseRun(opts);
296
296
  });
297
297
 
298
- // ── Runtime probe command (HG-01 HARD GATE) ─────────────────────────────────
298
+ // 鈹€鈹€ Runtime probe command (HG-01 HARD GATE) 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
299
299
 
300
300
  const runtimeCmd = program
301
301
  .command('runtime')
@@ -316,7 +316,7 @@ const synthCmd = runtimeCmd
316
316
 
317
317
  synthCmd
318
318
  .command('baseline')
319
- .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')
320
320
  .option('-w, --workspace <path>', 'Workspace directory (default: temp workspace)')
321
321
  .option('--json', 'Output raw JSON')
322
322
  .action(async (opts) => {
@@ -325,7 +325,7 @@ synthCmd
325
325
 
326
326
  synthCmd
327
327
  .command('flood')
328
- .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')
329
329
  .option('-w, --workspace <path>', 'Workspace directory (default: temp workspace)')
330
330
  .option('--json', 'Output raw JSON')
331
331
  .option('--identical-count <n>', 'Number of identical pain signals (default: 10)', parseInt)
@@ -343,7 +343,7 @@ synthCmd
343
343
 
344
344
  synthCmd
345
345
  .command('proven-channel')
346
- .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')
347
347
  .option('-w, --workspace <path>', 'Workspace directory (default: temp workspace)')
348
348
  .option('--json', 'Output raw JSON')
349
349
  .option('--channels <channels>', 'Comma-separated channel list (prompt,code_tool_hook,defer_archive)')
@@ -373,7 +373,7 @@ const demoCmd = program
373
373
 
374
374
  demoCmd
375
375
  .command('story-a')
376
- .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')
377
377
  .option('-w, --workspace <path>', 'Workspace directory (default: temp workspace)')
378
378
  .option('--json', 'Output raw JSON')
379
379
  .option('--channels <channels>', 'Comma-separated channel list (prompt,code_tool_hook,defer_archive)')
@@ -392,13 +392,13 @@ runtimeCmd
392
392
  .option('--openclaw-local', 'Use local OpenClaw (mutually exclusive with --openclaw-gateway)')
393
393
  .option('--openclaw-gateway', 'Use gateway OpenClaw (mutually exclusive with --openclaw-local)')
394
394
  .option('-a, --agent <agentId>', 'Agent ID to probe')
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
+ .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')
399
399
  .option('--maxRetries <n>', 'Max retry attempts for LLM failures', parseInt)
400
400
  .option('--timeoutMs <ms>', 'Timeout in milliseconds for probe', parseInt)
401
- .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')
402
402
  .option('--json', 'Output raw JSON')
403
403
  .action(async (opts) => {
404
404
  await handleRuntimeProbe(opts);
@@ -462,17 +462,17 @@ runtimeHealthCmd
462
462
 
463
463
  runtimeHealthCmd
464
464
  .command('gfi')
465
- .description('GFI workspace snapshot active vs stale session breakdown')
465
+ .description('GFI workspace snapshot 鈥?active vs stale session breakdown')
466
466
  .option('-w, --workspace <path>', 'Workspace directory')
467
467
  .option('--json', 'Output raw JSON')
468
468
  .action(async (opts) => {
469
469
  await handleRuntimeGfiSnapshot({ workspace: opts.workspace, json: opts.json });
470
470
  });
471
471
 
472
- // 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)
473
473
  runtimeCmd
474
474
  .command('gfi')
475
- .description('GFI workspace snapshot active vs stale session breakdown')
475
+ .description('GFI workspace snapshot 鈥?active vs stale session breakdown')
476
476
  .command('snapshot')
477
477
  .description('GFI workspace snapshot for the operator (alias: pd runtime health gfi)')
478
478
  .option('-w, --workspace <path>', 'Workspace directory')
@@ -608,7 +608,7 @@ const pruningCmd = runtimeCmd
608
608
 
609
609
  pruningCmd
610
610
  .command('report')
611
- .description('Show pruning health report watch/review principle signals')
611
+ .description('Show pruning health report 鈥?watch/review principle signals')
612
612
  .option('-w, --workspace <path>', 'Workspace directory')
613
613
  .option('--json', 'Output raw JSON')
614
614
  .action((opts) => {
@@ -679,7 +679,7 @@ pruningCmd
679
679
  });
680
680
  });
681
681
 
682
- // ── Candidate inspection commands ───────────────────────────────────────────
682
+ // 鈹€鈹€ Candidate inspection commands 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
683
683
 
684
684
  const candidateCmd = program
685
685
  .command('candidate')
@@ -783,7 +783,7 @@ candidateInternalizationCmd
783
783
  await handleCandidateInternalizationBackfill({ workspace: opts.workspace, dryRun: opts.dryRun, confirm: opts.confirm, includePending: opts.includePending, json: opts.json });
784
784
  });
785
785
 
786
- // ── Artifact inspection commands ────────────────────────────────────────────
786
+ // 鈹€鈹€ Artifact inspection commands 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
787
787
 
788
788
  const artifactCmd = program
789
789
  .command('artifact')
@@ -830,4 +830,22 @@ program
830
830
  });
831
831
  });
832
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
+
833
851
  program.parse();