@principles/pd-cli 1.142.3 → 1.143.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.
- package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts +9 -0
- package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts.map +1 -0
- package/dist/commands/__tests__/telemetry-flag-wiring.test.js +76 -0
- package/dist/commands/__tests__/telemetry-flag-wiring.test.js.map +1 -0
- package/dist/commands/__tests__/version.test.d.ts +2 -0
- package/dist/commands/__tests__/version.test.d.ts.map +1 -0
- package/dist/commands/__tests__/version.test.js +134 -0
- package/dist/commands/__tests__/version.test.js.map +1 -0
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +29 -10
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/pain-record.d.ts.map +1 -1
- package/dist/commands/pain-record.js +6 -15
- package/dist/commands/pain-record.js.map +1 -1
- package/dist/commands/runtime-activation.d.ts +29 -0
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +70 -31
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js +2 -2
- package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
- package/dist/commands/runtime-internalization-retry.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-retry.js +14 -0
- package/dist/commands/runtime-internalization-retry.js.map +1 -1
- package/dist/commands/telemetry.d.ts +82 -0
- package/dist/commands/telemetry.d.ts.map +1 -0
- package/dist/commands/telemetry.js +268 -0
- package/dist/commands/telemetry.js.map +1 -0
- package/dist/commands/version.d.ts +11 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +48 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/index.js +35 -2
- package/dist/index.js.map +1 -1
- package/dist/services/version-report.d.ts +44 -0
- package/dist/services/version-report.d.ts.map +1 -0
- package/dist/services/version-report.js +152 -0
- package/dist/services/version-report.js.map +1 -0
- package/package.json +2 -1
- package/src/commands/__tests__/telemetry-flag-wiring.test.ts +84 -0
- package/src/commands/__tests__/version.test.ts +141 -0
- package/src/commands/console.ts +34 -11
- package/src/commands/pain-record.ts +6 -13
- package/src/commands/runtime-activation.ts +74 -22
- package/src/commands/runtime-internalization-enqueue-successors.ts +3 -2
- package/src/commands/runtime-internalization-retry.ts +14 -0
- package/src/commands/telemetry.ts +349 -0
- package/src/commands/version.ts +49 -0
- package/src/index.ts +37 -2
- package/src/services/version-report.ts +203 -0
- package/tests/commands/console-open.test.ts +55 -5
- package/tests/commands/pain-record.test.ts +26 -0
- package/tests/commands/runtime-activation-shadow-telemetry.test.ts +130 -0
- package/tests/commands/runtime-activation.test.ts +67 -1
- package/tests/commands/runtime-internalization-enqueue-successors.test.ts +6 -1
- package/tests/commands/telemetry.test.ts +190 -0
- package/tests/e2e/cli-full-flow.test.ts +38 -3
|
@@ -23,7 +23,9 @@ import {
|
|
|
23
23
|
SqliteActivationSafetyStore,
|
|
24
24
|
collectOpenClawPromotionChecks,
|
|
25
25
|
summarizeRuleCodeShadowEvents,
|
|
26
|
+
buildPromotionEvidenceSnapshot,
|
|
26
27
|
} from '@principles/core/runtime-v2';
|
|
28
|
+
import { resolveOwnerIdentity, defaultOwnerIdentityHomeDir } from '@principles/core/runtime-v2';
|
|
27
29
|
import type {
|
|
28
30
|
ActivationDecision,
|
|
29
31
|
PIArtifactSnapshot,
|
|
@@ -53,11 +55,65 @@ function unavailableShadowSummary(): PromotionEvidenceSnapshot['shadowSummary']
|
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
/**
|
|
59
|
+
* PRI-577: RuleCode event telemetry candidate directories, in priority order.
|
|
60
|
+
*
|
|
61
|
+
* Runtime V2 convention is `.pd/logs`, but the v1 EventLog writer
|
|
62
|
+
* (openclaw-plugin `src/core/event-log.ts`) still emits `events_*.jsonl` under
|
|
63
|
+
* `.state/logs`. No production code ever created `.pd/logs`, so scanning only
|
|
64
|
+
* that path made every shadow metric report "unavailable" while 2500+ real
|
|
65
|
+
* evaluations sat unread in `.state/logs`. Readers scan both candidates until
|
|
66
|
+
* the writer migrates (ERR-031: both readers derive from this same list).
|
|
67
|
+
*/
|
|
68
|
+
export const RULECODE_EVENT_LOG_CANDIDATE_DIRS: readonly string[] = ['.pd/logs', '.state/logs'];
|
|
69
|
+
|
|
70
|
+
export interface CollectedRuleCodeEventEntries {
|
|
71
|
+
entries: unknown[];
|
|
72
|
+
/** Number of candidate directories that actually exist on disk. */
|
|
73
|
+
sourceDirsFound: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Collect rulehost telemetry entries from all candidate log directories.
|
|
78
|
+
* Malformed lines are excluded individually. A directory counts as a source
|
|
79
|
+
* only after it can be enumerated, so an unreadable path cannot be mistaken
|
|
80
|
+
* for a healthy channel with zero events (ERR-002).
|
|
81
|
+
*
|
|
82
|
+
* Exact lines copied between candidate directories are deduplicated by
|
|
83
|
+
* priority. Different events in same-named daily files are retained.
|
|
84
|
+
*/
|
|
85
|
+
export function collectRuleCodeEventEntries(workspaceDir: string): CollectedRuleCodeEventEntries {
|
|
58
86
|
const entries: unknown[] = [];
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
let sourceDirsFound = 0;
|
|
88
|
+
const higherPriorityLines = new Set<string>();
|
|
89
|
+
for (const candidate of RULECODE_EVENT_LOG_CANDIDATE_DIRS) {
|
|
90
|
+
const logsDir = path.join(workspaceDir, ...candidate.split('/'));
|
|
91
|
+
if (!fs.existsSync(logsDir)) continue;
|
|
92
|
+
try {
|
|
93
|
+
const files = fs.readdirSync(logsDir)
|
|
94
|
+
.filter(name => /^events_.*\.jsonl$/.test(name))
|
|
95
|
+
.sort()
|
|
96
|
+
.slice(-7);
|
|
97
|
+
sourceDirsFound += 1;
|
|
98
|
+
const currentSourceLines: string[] = [];
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const lines = fs.readFileSync(path.join(logsDir, file), 'utf8').split('\n').filter(Boolean);
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
currentSourceLines.push(line);
|
|
103
|
+
if (higherPriorityLines.has(line)) continue;
|
|
104
|
+
try { entries.push(JSON.parse(line) as unknown); } catch { /* exclude malformed telemetry */ }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const line of currentSourceLines) higherPriorityLines.add(line);
|
|
108
|
+
} catch { /* unreadable directory contributes no entries */ }
|
|
109
|
+
}
|
|
110
|
+
return { entries, sourceDirsFound };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Exported for PRI-577 regression tests and reuse by promote evidence assembly. */
|
|
114
|
+
export function readShadowSummaryForActivation(workspaceDir: string, activationId: string): PromotionEvidenceSnapshot['shadowSummary'] {
|
|
115
|
+
const { entries, sourceDirsFound } = collectRuleCodeEventEntries(workspaceDir);
|
|
116
|
+
if (sourceDirsFound === 0) return unavailableShadowSummary();
|
|
61
117
|
return summarizeRuleCodeShadowEvents(entries, activationId);
|
|
62
118
|
}
|
|
63
119
|
|
|
@@ -445,8 +501,9 @@ export async function handleRuntimeActivationPromote(opts: ActivationPromoteOpti
|
|
|
445
501
|
try {
|
|
446
502
|
const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
|
|
447
503
|
const flags = computeFlagsFromLoadResult(loadPdConfig(workspaceDir));
|
|
448
|
-
|
|
449
|
-
const
|
|
504
|
+
// ADR-0022 (PRI-578): single resolver — env > ~/.pd/owner.json > none
|
|
505
|
+
const identity = resolveOwnerIdentity(process.env, defaultOwnerIdentityHomeDir());
|
|
506
|
+
const { ownerId, credentialId } = identity;
|
|
450
507
|
const consoleToken = process.env.PD_CONSOLE_TOKEN?.trim();
|
|
451
508
|
const operatorId = process.env.USERNAME?.trim() || process.env.USER?.trim();
|
|
452
509
|
const actor = ownerId && credentialId && consoleToken
|
|
@@ -498,22 +555,17 @@ export async function handleRuntimeActivationPromote(opts: ActivationPromoteOpti
|
|
|
498
555
|
validateProductionArtifact: value => writer.canActivate(value),
|
|
499
556
|
});
|
|
500
557
|
},
|
|
501
|
-
buildEvidenceSnapshot: (checks, artifact) => {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
hostRuntimeVersion: 'openclaw-legacy@1', safetyGateResults: checks,
|
|
513
|
-
shadowSummary: readShadowSummary(workspaceDir, activationId),
|
|
514
|
-
configurationVersion: 'pd-config-current',
|
|
515
|
-
redaction: { version: 'v1', rawParametersStored: false }, createdAt,
|
|
516
|
-
};
|
|
558
|
+
buildEvidenceSnapshot: (checks, artifact, evaluationId) => {
|
|
559
|
+
return buildPromotionEvidenceSnapshot({
|
|
560
|
+
activationId,
|
|
561
|
+
evaluationId,
|
|
562
|
+
checks,
|
|
563
|
+
artifact,
|
|
564
|
+
expectedArtifactDigest: request.expectedArtifactDigest,
|
|
565
|
+
ownerIdentity: actor,
|
|
566
|
+
hostRuntimeVersion: 'openclaw-legacy@1',
|
|
567
|
+
shadowSummary: readShadowSummaryForActivation(workspaceDir, activationId),
|
|
568
|
+
});
|
|
517
569
|
},
|
|
518
570
|
newEvaluationId: () => `readiness-${randomUUID()}`,
|
|
519
571
|
});
|
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
InternalizationOrchestrator,
|
|
5
5
|
isPeerRunnerKind,
|
|
6
6
|
hydratePITaskRecord,
|
|
7
|
+
PD_TASK_STATUSES,
|
|
7
8
|
} from '@principles/core/runtime-v2';
|
|
8
|
-
import type { CommitNextTaskResult } from '@principles/core/runtime-v2';
|
|
9
|
+
import type { CommitNextTaskResult, PDTaskStatus } from '@principles/core/runtime-v2';
|
|
9
10
|
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
10
11
|
|
|
11
12
|
interface EnqueueSuccessorsOptions {
|
|
@@ -236,7 +237,7 @@ interface SuccessorIndexEntry {
|
|
|
236
237
|
async function buildSuccessorIndex(
|
|
237
238
|
stateManager: RuntimeStateManager,
|
|
238
239
|
): Promise<Map<string, SuccessorIndexEntry>> {
|
|
239
|
-
const allStatuses:
|
|
240
|
+
const allStatuses: PDTaskStatus[] = [...PD_TASK_STATUSES];
|
|
240
241
|
const index = new Map<string, SuccessorIndexEntry>();
|
|
241
242
|
const results = await Promise.all(
|
|
242
243
|
allStatuses.map(status => stateManager.listTasks({ status })),
|
|
@@ -137,6 +137,20 @@ export async function handleRuntimeInternalizationRetry(opts: InternalizationRet
|
|
|
137
137
|
emit(out, opts.json);
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
|
+
if (outcome.status === 'rejected') {
|
|
141
|
+
// PRI-629 Recover guard: decision-capable 人工裁决不走 authority reset —
|
|
142
|
+
// 治理出口在 Console 治理焦点 (或 owner-decisions API)。
|
|
143
|
+
const out: InternalizationRetryOutput = {
|
|
144
|
+
status: 'skipped',
|
|
145
|
+
taskId: opts.taskId,
|
|
146
|
+
taskKind: outcome.taskKind,
|
|
147
|
+
previousStatus: outcome.previousStatus,
|
|
148
|
+
reason: outcome.reason,
|
|
149
|
+
nextAction: 'This task awaits an Owner decision (accept / revise once / reject). Resolve it in the Console governance focus, or via the owner-decisions API. Recover is not a governance exit.',
|
|
150
|
+
};
|
|
151
|
+
emit(out, opts.json);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
140
154
|
if (outcome.status === 'metadata_invalid') {
|
|
141
155
|
// fail closed: 只改 status 会把(可能损坏的)旧 authority 记录原样留在
|
|
142
156
|
// metadata 里,下一次 run 由它接管 —— 产生 partial retry。
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pd telemetry — Anonymous Product Telemetry v1 control plane
|
|
3
|
+
* (PRI-597, SPEC §40-§43).
|
|
4
|
+
*
|
|
5
|
+
* Consent lives at the machine level (~/.pd/product-telemetry.json via
|
|
6
|
+
* host-runtime); measurement (daily ID, dedup, retry, lock) is
|
|
7
|
+
* workspace-scoped. The release feature flag (anonymous_product_telemetry)
|
|
8
|
+
* is an INDEPENDENT gate read from the resolved workspace's .pd/config.yaml.
|
|
9
|
+
*
|
|
10
|
+
* Commands:
|
|
11
|
+
* pd telemetry status — gates, consent, eligibility, bounded export status
|
|
12
|
+
* pd telemetry enable — explicit consent (default dry-run; --confirm to write)
|
|
13
|
+
* pd telemetry disable — deny consent + delete local identity (default dry-run)
|
|
14
|
+
* pd telemetry reset — rotate/delete identity (default dry-run)
|
|
15
|
+
* pd telemetry preview — exact outbound payload; never sends
|
|
16
|
+
*
|
|
17
|
+
* --json outputs exactly one parseable object; failures carry reason +
|
|
18
|
+
* nextAction; the telemetry secret is never printed.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import type { Command } from 'commander';
|
|
23
|
+
import {
|
|
24
|
+
createProductTelemetryService,
|
|
25
|
+
PREVIEW_BANNER,
|
|
26
|
+
type ProductTelemetryStatusView,
|
|
27
|
+
} from '@principles/host-runtime';
|
|
28
|
+
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
29
|
+
import { emitError, emitFlagConflict, emitResult } from '../services/cli-output.js';
|
|
30
|
+
|
|
31
|
+
// ── Output types ─────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface TelemetryStatusOutput {
|
|
34
|
+
status: 'ok' | 'degraded' | 'failed';
|
|
35
|
+
command: 'telemetry:status';
|
|
36
|
+
consent: string;
|
|
37
|
+
consentVersion: string;
|
|
38
|
+
hasSecret: boolean;
|
|
39
|
+
flagEnabled: boolean | null;
|
|
40
|
+
flagSource: string | null;
|
|
41
|
+
environmentSuppressed: boolean;
|
|
42
|
+
suppressionReasons: string[];
|
|
43
|
+
canExport: boolean;
|
|
44
|
+
blockers: string[];
|
|
45
|
+
lastAttemptedAt?: string;
|
|
46
|
+
lastSucceededAt?: string;
|
|
47
|
+
lastFailureCode?: string;
|
|
48
|
+
nextRetryAt?: string;
|
|
49
|
+
endpoint: string;
|
|
50
|
+
workspaceDir?: string;
|
|
51
|
+
nextAction?: string;
|
|
52
|
+
reason?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TelemetryMutationOutput {
|
|
56
|
+
status: 'ok' | 'failed';
|
|
57
|
+
command: 'telemetry:enable' | 'telemetry:disable' | 'telemetry:reset';
|
|
58
|
+
dryRun: boolean;
|
|
59
|
+
applied: boolean;
|
|
60
|
+
consent?: string;
|
|
61
|
+
/** What the applied/dry-run mutation changes, in plain terms. */
|
|
62
|
+
effect: string;
|
|
63
|
+
reason?: string;
|
|
64
|
+
nextAction?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TelemetryPreviewOutput {
|
|
68
|
+
status: 'ok';
|
|
69
|
+
command: 'telemetry:preview';
|
|
70
|
+
banner: string;
|
|
71
|
+
snapshot: Record<string, unknown>;
|
|
72
|
+
gates: {
|
|
73
|
+
flagEnabled: boolean | null;
|
|
74
|
+
consent: string;
|
|
75
|
+
environmentSuppressed: boolean;
|
|
76
|
+
suppressionReasons: string[];
|
|
77
|
+
canExport: boolean;
|
|
78
|
+
blockers: string[];
|
|
79
|
+
};
|
|
80
|
+
notes: string[];
|
|
81
|
+
secretEphemeral: boolean;
|
|
82
|
+
nextAction?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Text formatting ──────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
function formatStatusText(output: TelemetryStatusOutput): string {
|
|
88
|
+
const lines: string[] = [];
|
|
89
|
+
lines.push('PD Anonymous Product Telemetry');
|
|
90
|
+
lines.push(`consent: ${output.consent} (v${output.consentVersion})`);
|
|
91
|
+
lines.push(`feature flag: ${output.flagEnabled === null ? 'unknown (no workspace)' : output.flagEnabled ? 'on' : 'off'}${output.flagSource ? ` (${output.flagSource})` : ''}`);
|
|
92
|
+
lines.push(`environment: ${output.environmentSuppressed ? `suppressed (${output.suppressionReasons.join(', ')})` : 'eligible'}`);
|
|
93
|
+
lines.push(`can export: ${output.canExport ? 'yes' : `no (${output.blockers.join(', ')})`}`);
|
|
94
|
+
lines.push(`local secret: ${output.hasSecret ? 'present' : 'absent'}`);
|
|
95
|
+
if (output.lastAttemptedAt) lines.push(`last attempted: ${output.lastAttemptedAt}`);
|
|
96
|
+
if (output.lastSucceededAt) lines.push(`last succeeded: ${output.lastSucceededAt}`);
|
|
97
|
+
if (output.lastFailureCode) lines.push(`last failure: ${output.lastFailureCode}`);
|
|
98
|
+
if (output.nextRetryAt) lines.push(`next retry: ${output.nextRetryAt}`);
|
|
99
|
+
lines.push(`collector: ${output.endpoint}`);
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push('Collects: PD version, anonymous tri-state milestones (true/false/unavailable), coarse reliability.');
|
|
102
|
+
lines.push('Never collects: conversations, code/files, Principle content, Pain content.');
|
|
103
|
+
if (output.nextAction) lines.push(`Next action: ${output.nextAction}`);
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatMutationText(output: TelemetryMutationOutput): string {
|
|
108
|
+
const lines: string[] = [];
|
|
109
|
+
lines.push(`PD telemetry ${output.command.split(':')[1]} ${output.applied ? 'applied' : '(dry-run — not applied)'}`);
|
|
110
|
+
lines.push(output.effect);
|
|
111
|
+
if (!output.applied) lines.push('Re-run with --confirm to apply.');
|
|
112
|
+
if (output.nextAction) lines.push(`Next action: ${output.nextAction}`);
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatPreviewText(output: TelemetryPreviewOutput): string {
|
|
117
|
+
const lines: string[] = [];
|
|
118
|
+
lines.push(output.secretEphemeral
|
|
119
|
+
? 'PD Telemetry Preview — exact payload shape (dailyTelemetryId is provisional until enabled)'
|
|
120
|
+
: 'PD Telemetry Preview — exact outbound payload');
|
|
121
|
+
lines.push('');
|
|
122
|
+
lines.push(JSON.stringify(output.snapshot, null, 2));
|
|
123
|
+
lines.push('');
|
|
124
|
+
lines.push(`Collected: PD version, anonymous tri-state milestones (true/false/unavailable), coarse reliability.`);
|
|
125
|
+
lines.push(`Never collected: conversations, code/files, Principle content, Pain content.`);
|
|
126
|
+
lines.push(`Gates: flag=${output.gates.flagEnabled ?? 'unresolved'} consent=${output.gates.consent} environment=${output.gates.environmentSuppressed ? 'suppressed' : 'eligible'} → wouldExport=${output.gates.canExport}`);
|
|
127
|
+
if (output.notes.length > 0) {
|
|
128
|
+
lines.push('Notes:');
|
|
129
|
+
for (const note of output.notes) lines.push(` [!] ${note}`);
|
|
130
|
+
}
|
|
131
|
+
lines.push('');
|
|
132
|
+
lines.push(`>>> ${PREVIEW_BANNER}`);
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Shared helpers ───────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
interface TelemetryOptions {
|
|
139
|
+
workspace?: string;
|
|
140
|
+
json?: boolean;
|
|
141
|
+
confirm?: boolean;
|
|
142
|
+
dryRun?: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveWorkspaceOrNull(opts: TelemetryOptions): string | undefined {
|
|
146
|
+
if (opts.workspace) return path.resolve(opts.workspace);
|
|
147
|
+
try {
|
|
148
|
+
return resolveWorkspaceDir();
|
|
149
|
+
} catch {
|
|
150
|
+
// Consent/identity commands are machine-scope; a workspace is only needed
|
|
151
|
+
// to report the workspace-scope flag gate. Absent workspace = flag unknown.
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function makeService() {
|
|
157
|
+
return createProductTelemetryService({});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function checkDryRunConfirmMutex(opts: TelemetryOptions, json: boolean): boolean {
|
|
161
|
+
if (opts.dryRun === true && opts.confirm === true) {
|
|
162
|
+
process.exitCode = emitFlagConflict({ json });
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Handlers ─────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
export async function handleTelemetryStatus(opts: TelemetryOptions): Promise<void> {
|
|
171
|
+
const service = makeService();
|
|
172
|
+
const workspaceDir = resolveWorkspaceOrNull(opts);
|
|
173
|
+
const result = service.getStatus(workspaceDir);
|
|
174
|
+
if (!result.ok) {
|
|
175
|
+
process.exitCode = emitError(new Error(result.reason), { json: opts.json ?? false, nextAction: result.nextAction });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const view: ProductTelemetryStatusView = result.view;
|
|
179
|
+
const output: TelemetryStatusOutput = {
|
|
180
|
+
status: 'ok',
|
|
181
|
+
command: 'telemetry:status',
|
|
182
|
+
consent: view.consent,
|
|
183
|
+
consentVersion: view.consentVersion,
|
|
184
|
+
hasSecret: view.hasSecret,
|
|
185
|
+
flagEnabled: view.flagEnabled,
|
|
186
|
+
flagSource: view.flagSource,
|
|
187
|
+
environmentSuppressed: view.environmentSuppressed,
|
|
188
|
+
suppressionReasons: view.suppressionReasons,
|
|
189
|
+
canExport: view.canExport,
|
|
190
|
+
blockers: view.blockers,
|
|
191
|
+
...(view.lastAttemptedAt !== undefined ? { lastAttemptedAt: view.lastAttemptedAt } : {}),
|
|
192
|
+
...(view.lastSucceededAt !== undefined ? { lastSucceededAt: view.lastSucceededAt } : {}),
|
|
193
|
+
...(view.lastFailureCode !== undefined ? { lastFailureCode: view.lastFailureCode } : {}),
|
|
194
|
+
...(view.nextRetryAt !== undefined ? { nextRetryAt: view.nextRetryAt } : {}),
|
|
195
|
+
endpoint: view.endpoint,
|
|
196
|
+
...(workspaceDir !== undefined ? { workspaceDir } : {}),
|
|
197
|
+
...(view.nextAction !== undefined ? { nextAction: view.nextAction } : {}),
|
|
198
|
+
};
|
|
199
|
+
emitResult(output, { json: opts.json ?? false, formatText: formatStatusText });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
type MutationKind = 'enable' | 'disable' | 'reset';
|
|
203
|
+
|
|
204
|
+
const MUTATION_EFFECTS: Record<MutationKind, string> = {
|
|
205
|
+
enable: 'Records explicit telemetry consent (granted) and creates a local random secret for daily unlinkable IDs. Nothing is exported unless the feature flag and environment eligibility also allow it.',
|
|
206
|
+
disable: 'Records consent denied and deletes the local telemetry secret and export status. Future telemetry export requests: 0.',
|
|
207
|
+
reset: 'Deletes the local telemetry secret and export status. If consent remains granted, a fresh secret is generated — future daily IDs are unrelated to previous ones.',
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const MUTATION_NEXT_ACTIONS: Record<MutationKind, string> = {
|
|
211
|
+
enable: 'Run "pd telemetry preview" to inspect the exact outbound payload.',
|
|
212
|
+
disable: 'Telemetry is fully off. Re-enable anytime with "pd telemetry enable --confirm".',
|
|
213
|
+
reset: 'Previous daily IDs can no longer be derived. See "pd telemetry status".',
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export async function handleTelemetryMutation(kind: MutationKind, opts: TelemetryOptions): Promise<void> {
|
|
217
|
+
const json = opts.json ?? false;
|
|
218
|
+
if (!checkDryRunConfirmMutex(opts, json)) return;
|
|
219
|
+
const applied = opts.confirm === true;
|
|
220
|
+
const service = makeService();
|
|
221
|
+
|
|
222
|
+
if (!applied) {
|
|
223
|
+
const output: TelemetryMutationOutput = {
|
|
224
|
+
status: 'ok',
|
|
225
|
+
command: `telemetry:${kind}`,
|
|
226
|
+
dryRun: true,
|
|
227
|
+
applied: false,
|
|
228
|
+
effect: MUTATION_EFFECTS[kind],
|
|
229
|
+
nextAction: 'Re-run with --confirm to apply.',
|
|
230
|
+
};
|
|
231
|
+
emitResult(output, { json, formatText: formatMutationText });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const result =
|
|
236
|
+
kind === 'enable' ? service.enable() : kind === 'disable' ? service.disable() : service.reset();
|
|
237
|
+
if (!result.ok) {
|
|
238
|
+
process.exitCode = emitError(new Error(result.reason), { json, nextAction: result.nextAction });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const output: TelemetryMutationOutput = {
|
|
242
|
+
status: 'ok',
|
|
243
|
+
command: `telemetry:${kind}`,
|
|
244
|
+
dryRun: false,
|
|
245
|
+
applied: true,
|
|
246
|
+
consent: result.consent,
|
|
247
|
+
effect: MUTATION_EFFECTS[kind],
|
|
248
|
+
nextAction: MUTATION_NEXT_ACTIONS[kind],
|
|
249
|
+
};
|
|
250
|
+
emitResult(output, { json, formatText: formatMutationText });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function handleTelemetryPreview(opts: TelemetryOptions): Promise<void> {
|
|
254
|
+
let workspaceDir: string;
|
|
255
|
+
if (opts.workspace) {
|
|
256
|
+
workspaceDir = path.resolve(opts.workspace);
|
|
257
|
+
} else {
|
|
258
|
+
try {
|
|
259
|
+
workspaceDir = resolveWorkspaceDir();
|
|
260
|
+
} catch (error) {
|
|
261
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
262
|
+
process.exitCode = emitError(new Error(`workspace_unresolvable: ${message}`), {
|
|
263
|
+
json: opts.json ?? false,
|
|
264
|
+
nextAction: 'Pass --workspace <path> or run from a PD workspace',
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const service = makeService();
|
|
270
|
+
const preview = service.preview(workspaceDir);
|
|
271
|
+
const output: TelemetryPreviewOutput = {
|
|
272
|
+
status: 'ok',
|
|
273
|
+
command: 'telemetry:preview',
|
|
274
|
+
banner: PREVIEW_BANNER,
|
|
275
|
+
snapshot: preview.snapshot,
|
|
276
|
+
gates: {
|
|
277
|
+
flagEnabled: preview.gates.flagEnabled,
|
|
278
|
+
consent: preview.gates.consent,
|
|
279
|
+
environmentSuppressed: preview.gates.environmentSuppressed,
|
|
280
|
+
suppressionReasons: preview.gates.suppressionReasons,
|
|
281
|
+
canExport: preview.gates.canExport,
|
|
282
|
+
blockers: preview.gates.blockers,
|
|
283
|
+
},
|
|
284
|
+
notes: preview.notes,
|
|
285
|
+
secretEphemeral: preview.secretEphemeral,
|
|
286
|
+
};
|
|
287
|
+
emitResult(output, { json: opts.json ?? false, formatText: formatPreviewText });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Registration (used by index.ts and wiring tests) ─────────────────────────
|
|
291
|
+
|
|
292
|
+
export function registerTelemetryCommand(parent: Command): Command {
|
|
293
|
+
const telemetry = parent
|
|
294
|
+
.command('telemetry')
|
|
295
|
+
.description('Anonymous product telemetry control (opt-in, privacy-preserving)');
|
|
296
|
+
|
|
297
|
+
telemetry
|
|
298
|
+
.command('status')
|
|
299
|
+
.description('Show telemetry consent, gates, eligibility, and bounded export status')
|
|
300
|
+
.option('-w, --workspace <path>', 'Workspace directory (for the feature-flag gate)')
|
|
301
|
+
.option('--json', 'Output raw JSON')
|
|
302
|
+
.action(async (opts) => {
|
|
303
|
+
await handleTelemetryStatus({ workspace: opts.workspace, json: opts.json });
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
telemetry
|
|
307
|
+
.command('enable')
|
|
308
|
+
.description('Grant explicit telemetry consent (default OFF until enabled)')
|
|
309
|
+
.option('--dry-run', 'Show the effect without writing (default)')
|
|
310
|
+
.option('--confirm', 'Apply the consent change (required to write)')
|
|
311
|
+
.option('-w, --workspace <path>', 'Workspace directory (reporting only)')
|
|
312
|
+
.option('--json', 'Output raw JSON')
|
|
313
|
+
.action(async (opts) => {
|
|
314
|
+
await handleTelemetryMutation('enable', { workspace: opts.workspace, json: opts.json, confirm: opts.confirm, dryRun: opts.dryRun });
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
telemetry
|
|
318
|
+
.command('disable')
|
|
319
|
+
.description('Deny consent and delete the local telemetry identity (zero future exports)')
|
|
320
|
+
.option('--dry-run', 'Show the effect without writing (default)')
|
|
321
|
+
.option('--confirm', 'Apply the change (required to write)')
|
|
322
|
+
.option('-w, --workspace <path>', 'Workspace directory (reporting only)')
|
|
323
|
+
.option('--json', 'Output raw JSON')
|
|
324
|
+
.action(async (opts) => {
|
|
325
|
+
await handleTelemetryMutation('disable', { workspace: opts.workspace, json: opts.json, confirm: opts.confirm, dryRun: opts.dryRun });
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
telemetry
|
|
329
|
+
.command('reset')
|
|
330
|
+
.description('Delete and rotate the local telemetry identity (unlink from previous daily IDs)')
|
|
331
|
+
.option('--dry-run', 'Show the effect without writing (default)')
|
|
332
|
+
.option('--confirm', 'Apply the change (required to write)')
|
|
333
|
+
.option('-w, --workspace <path>', 'Workspace directory (reporting only)')
|
|
334
|
+
.option('--json', 'Output raw JSON')
|
|
335
|
+
.action(async (opts) => {
|
|
336
|
+
await handleTelemetryMutation('reset', { workspace: opts.workspace, json: opts.json, confirm: opts.confirm, dryRun: opts.dryRun });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
telemetry
|
|
340
|
+
.command('preview')
|
|
341
|
+
.description('Show the exact outbound telemetry payload (nothing is sent)')
|
|
342
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
343
|
+
.option('--json', 'Output raw JSON')
|
|
344
|
+
.action(async (opts) => {
|
|
345
|
+
await handleTelemetryPreview({ workspace: opts.workspace, json: opts.json });
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return telemetry;
|
|
349
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pd version command (SPEC §12).
|
|
3
|
+
*
|
|
4
|
+
* Human output prints the short stable version text; `--json` emits exactly
|
|
5
|
+
* one parseable JSON object with the full canonical report (productVersion,
|
|
6
|
+
* releaseId, components, bootstrapVersion, channel, source, generation,
|
|
7
|
+
* health, lastTransaction).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
import { buildVersionReport, formatShortVersion, VersionReportError } from '../services/version-report.js';
|
|
12
|
+
|
|
13
|
+
async function runVersion(json: boolean): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
const report = buildVersionReport();
|
|
16
|
+
if (json) {
|
|
17
|
+
console.log(JSON.stringify(report, null, 2));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log(formatShortVersion(report));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error instanceof VersionReportError) {
|
|
23
|
+
if (json) {
|
|
24
|
+
console.log(JSON.stringify({
|
|
25
|
+
ok: false,
|
|
26
|
+
reason: error.reason,
|
|
27
|
+
message: error.message,
|
|
28
|
+
nextAction: error.nextAction,
|
|
29
|
+
}, null, 2));
|
|
30
|
+
} else {
|
|
31
|
+
console.error(error.message);
|
|
32
|
+
console.error(`Next: ${error.nextAction}`);
|
|
33
|
+
}
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function registerVersionCommand(program: Command): void {
|
|
42
|
+
program
|
|
43
|
+
.command('version')
|
|
44
|
+
.description('Show the canonical PD product version and installation state')
|
|
45
|
+
.option('--json', 'Emit the full canonical version report as one JSON object', false)
|
|
46
|
+
.action(async (opts: Record<string, unknown>) => {
|
|
47
|
+
await runVersion(opts.json === true);
|
|
48
|
+
});
|
|
49
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { handleSamplesReview } from './commands/samples-review.js';
|
|
|
15
15
|
import { handleEvolutionTasksList } from './commands/evolution-tasks-list.js';
|
|
16
16
|
import { handleEvolutionTasksShow } from './commands/evolution-tasks-show.js';
|
|
17
17
|
import { registerHealthCommand } from './commands/health.js';
|
|
18
|
+
import { registerVersionCommand } from './commands/version.js';
|
|
19
|
+
import { buildVersionReport, formatShortVersion, VersionReportError } from './services/version-report.js';
|
|
18
20
|
import { handleTaskShow, registerTaskListCommand } from './commands/task.js';
|
|
19
21
|
import { handleRunList, handleRunShow } from './commands/run.js';
|
|
20
22
|
import { handleTrajectoryLocate } from './commands/trajectory.js';
|
|
@@ -68,6 +70,7 @@ import { registerRulecodeCommand } from './commands/rulecode.js';
|
|
|
68
70
|
import { registerIntentCommand } from './commands/intent.js';
|
|
69
71
|
import { registerErrorsListCommand } from './commands/errors-list.js';
|
|
70
72
|
import { registerPrinciplesCommand } from './commands/principles-stats.js';
|
|
73
|
+
import { registerTelemetryCommand } from './commands/telemetry.js';
|
|
71
74
|
|
|
72
75
|
import { createRequire } from 'module';
|
|
73
76
|
const require = createRequire(import.meta.url);
|
|
@@ -75,12 +78,38 @@ const pkg = require('../package.json') as { version: string };
|
|
|
75
78
|
|
|
76
79
|
const program = new Command();
|
|
77
80
|
|
|
81
|
+
// SPEC §12: `pd --version` prints the canonical product version when a
|
|
82
|
+
// supported installation exists; a development checkout falls back to the
|
|
83
|
+
// CLI package version with an explicit marker instead of impersonating an
|
|
84
|
+
// installed release.
|
|
85
|
+
function handleVersionFlag(args: readonly string[]): boolean {
|
|
86
|
+
if (!args.includes('--version') && !args.includes('-V')) return false;
|
|
87
|
+
try {
|
|
88
|
+
console.log(formatShortVersion(buildVersionReport()));
|
|
89
|
+
return true;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof VersionReportError && error.reason === 'not_installed') {
|
|
92
|
+
console.log(`Principles Disciple ${pkg.version} (development-checkout)`);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (error instanceof VersionReportError) {
|
|
96
|
+
console.error(error.message);
|
|
97
|
+
console.error(`Next: ${error.nextAction}`);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
program
|
|
79
106
|
.name('pd')
|
|
80
107
|
.description('PD CLI — Pain recording, sample management, and evolution tasks')
|
|
81
|
-
.
|
|
108
|
+
.option('-V, --version', 'output the canonical PD product version')
|
|
82
109
|
.enablePositionalOptions();
|
|
83
110
|
|
|
111
|
+
registerVersionCommand(program);
|
|
112
|
+
|
|
84
113
|
const painCmd = program
|
|
85
114
|
.command('pain')
|
|
86
115
|
.description('Pain signal management');
|
|
@@ -166,6 +195,10 @@ tasksCmd
|
|
|
166
195
|
|
|
167
196
|
registerHealthCommand(program);
|
|
168
197
|
|
|
198
|
+
// ── Anonymous Product Telemetry v1 control plane (PRI-595~603) ────────────────
|
|
199
|
+
|
|
200
|
+
registerTelemetryCommand(program);
|
|
201
|
+
|
|
169
202
|
// ── Runtime v2 task/run commands ──────────────────────────────────────────────鈹€鈹€鈹€鈹€鈹€鈹€
|
|
170
203
|
|
|
171
204
|
const rtTaskCmd = program
|
|
@@ -1098,4 +1131,6 @@ qualityCmd
|
|
|
1098
1131
|
await handleQualityScorecard(opts);
|
|
1099
1132
|
});
|
|
1100
1133
|
|
|
1101
|
-
|
|
1134
|
+
if (!handleVersionFlag(process.argv.slice(2))) {
|
|
1135
|
+
program.parse();
|
|
1136
|
+
}
|