@principles/pd-cli 1.149.0 → 1.149.1

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/src/index.ts CHANGED
@@ -105,7 +105,7 @@ function handleVersionFlag(args: readonly string[]): boolean {
105
105
 
106
106
  program
107
107
  .name('pd')
108
- .description('PD CLI — Pain recording, sample management, and evolution tasks')
108
+ .description('PD CLI — Pain recording, sample management, and evolution task history')
109
109
  .option('-V, --version', 'output the canonical PD product version')
110
110
  .enablePositionalOptions();
111
111
 
@@ -123,7 +123,10 @@ painCmd
123
123
  .option('-S, --source <text>', 'Source of the pain signal', 'manual')
124
124
  .option('-w, --workspace <path>', 'Workspace directory')
125
125
  .option('--session <id>', 'Session ID to bind (validated against this workspace\'s trajectory.db; without it the record is unbound: no trajectory evidence, candidates likely gated by the admission threshold)')
126
- .option('--host <kind>', 'PRI-743: explicit host attribution (openclaw | codex). openclaw = default attribution without the disclosure warning; codex refuses — the CLI cannot verify Codex lineage.', undefined)
126
+ .option('--host <kind>', 'PRI-743: explicit host attribution (openclaw | codex). openclaw = default attribution without the disclosure warning; codex requires the real Codex lineage (--rollout-id, --host-turn-id, and --session as the root session).', undefined)
127
+ .option('--rollout-id <id>', 'PRI-743: Codex rollout identity — required by --host codex', undefined)
128
+ .option('--host-turn-id <id>', 'PRI-743: Codex turn id — required by --host codex', undefined)
129
+ .option('--logical-key <key>', 'PRI-743: Codex logical observation key override for --host codex (default: codex|<rollout-id>|<host-turn-id>|user)', undefined)
127
130
  .option('--wait', 'Wait for diagnosis to complete (sync mode, overrides async flag)')
128
131
  .option('--json', 'Output raw JSON')
129
132
  .action(async (opts) => {
@@ -179,9 +182,15 @@ samplesCmd
179
182
  await handleSamplesReview({ sampleId, decision: decision === 'approve' ? 'approved' : 'rejected', note });
180
183
  });
181
184
 
185
+ // ── Evolution task reader (hidden) ────────────────────────────────────────────
186
+ // PRI-752 review: the worker is gone (flag retired to `gone`), but this
187
+ // read-only surface stays for historical `evolution_tasks` rows in already-
188
+ // provisioned workspaces — data-retention disposition is a separate Owner
189
+ // decision (PRI-737 reality audit §7.3). Table schema + quality-scorecard
190
+ // reader are retained accordingly.
182
191
  const evolutionCmd = program
183
192
  .command('evolution', { hidden: true })
184
- .description('Evolution task management');
193
+ .description('Evolution task management (historical data reader)');
185
194
 
186
195
  const tasksCmd = evolutionCmd
187
196
  .command('tasks')
@@ -54,6 +54,7 @@ import {
54
54
  // PRI-714: resolve outputLanguage from the same effective config the
55
55
  // runners already receive (EP-07: canonical resolved value, not raw input).
56
56
  resolveOutputLanguage,
57
+ buildArtificerHostSemanticContext,
57
58
  } from '@principles/core/runtime-v2';
58
59
  import type {
59
60
  AdversarialLoopResult,
@@ -411,12 +412,21 @@ export async function runRuleHostPipeline(opts: RuleHostPipelineOptions): Promis
411
412
  onProgress('adversarial_loop', 'failed', refusalReason);
412
413
  return rejectedResult(opts.painId, stages, `evaluator_runtime_context_unresolvable: ${evaluatorContext.reason} (nextAction: ${evaluatorContext.nextAction})`);
413
414
  }
415
+ // PRI-741: artificer generation + evaluator parity BOTH anchor on the ONE
416
+ // registry snapshot evaluatorContext resolved above (review round: a
417
+ // second independent resolveWorkspaceHostToolSemantics read could observe
418
+ // a different declaration snapshot than the replay gate). Unresolvable
419
+ // provenance already failed loud at the evaluatorContext refusal above.
420
+ const hostSemanticContext = buildArtificerHostSemanticContext(evaluatorContext.registry, evaluatorContext.hostKinds ?? []);
414
421
  const artificerRunner = new ArtificerRunner(
415
422
  {
416
423
  stateManager, runtimeAdapter: capability.artificerAdapter, eventEmitter, validator: new DefaultArtificerValidator(), artifactStore,
417
424
  contextMode: opts.contextMode ?? 'v1', behaviorExamplePack: opts.behaviorExamplePack, contentHashFn,
418
425
  },
419
- runnerOptsFor(capability.artificerAdapter),
426
+ {
427
+ ...runnerOptsFor(capability.artificerAdapter),
428
+ hostSemanticContext,
429
+ },
420
430
  );
421
431
  // PRI-510 (DEFECT-004): construct EvaluatorRunnerDeps via the centralized
422
432
  // helper so the repair-loop wiring (isRepairLoopEnabled + seeder) is
@@ -435,7 +445,13 @@ export async function runRuleHostPipeline(opts: RuleHostPipelineOptions): Promis
435
445
  // from durable provenance) — replaces the previous sandbox-only gate
436
446
  // deps so generation-time replay verdicts match the activation gate on
437
447
  // the same workspace. Options argument (2nd), never deps (1st).
438
- { ...runnerOptsFor(agentAdapters.evaluator), gateDeps: evaluatorContext.gateDeps },
448
+ {
449
+ ...runnerOptsFor(agentAdapters.evaluator),
450
+ gateDeps: evaluatorContext.gateDeps,
451
+ // PRI-741: host-name parity replay case from the SAME registry
452
+ // snapshot as the gateDeps above (single resolution).
453
+ hostSemanticContext,
454
+ },
439
455
  );
440
456
 
441
457
  const loopResult = await runAdversarialLoop({
@@ -93,7 +93,31 @@ describe('pd pain record --session (real Commander + real trajectory.db)', () =>
93
93
  // Commander wraps help text; normalize whitespace before asserting content.
94
94
  const normalized = result.stdout.replace(/\s+/g, ' ');
95
95
  expect(normalized).toContain('openclaw | codex');
96
- expect(normalized).toContain('codex refuses');
96
+ expect(normalized).toContain('codex requires the real Codex lineage');
97
+ }, 15_000);
98
+
99
+ it('PRI-743: --help registers the Codex lineage flags used by --host codex', async () => {
100
+ const result = await runBuiltCli(['pain', 'record', '--help']);
101
+ expect(result.status).toBe(0);
102
+ expect(result.stdout).toContain('--rollout-id <id>');
103
+ expect(result.stdout).toContain('--host-turn-id <id>');
104
+ expect(result.stdout).toContain('--logical-key <key>');
105
+ }, 15_000);
106
+
107
+ it('PRI-743: --host codex without lineage exits non-zero with codex_lineage_required', async () => {
108
+ const result = await runBuiltCli([
109
+ 'pain', 'record',
110
+ '--reason', 'parser test pain',
111
+ '--host', 'codex',
112
+ '--workspace', tmpDir,
113
+ '--json',
114
+ ]);
115
+
116
+ expect(result.status).not.toBe(0);
117
+ const parsed = JSON.parse(result.stdout.trim()) as Record<string, unknown>;
118
+ expect(parsed.status).toBe('failed');
119
+ expect(parsed.reason).toBe('codex_lineage_required');
120
+ expect(typeof parsed.nextAction).toBe('string');
97
121
  }, 15_000);
98
122
 
99
123
  it('PRI-743: an invalid --host value exits non-zero with a single structured JSON object', async () => {
@@ -414,24 +414,63 @@ describe('pd pain record', () => {
414
414
 
415
415
  const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]) as { hostAttribution?: string; warnings?: string[] };
416
416
  expect(jsonOutput.hostAttribution).toBe('openclaw');
417
- expect((jsonOutput.warnings ?? []).some((w) => w.includes("defaulted to 'openclaw'"))).toBe(false);
417
+ expect((jsonOutput.warnings ?? []).some((w) => w.includes('host kind not specified, defaulting to openclaw'))).toBe(false);
418
418
 
419
419
  logSpy.mockRestore();
420
420
  exitSpy.mockRestore();
421
421
  });
422
422
 
423
- it('PRI-743: --host codex refuses loudly — the CLI cannot verify Codex lineage (rc-6)', async () => {
423
+ it('PRI-743: --host codex records host_kind=codex with the caller-supplied Codex lineage', async () => {
424
424
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
425
425
  const exitSpy = mockProcessExit();
426
426
 
427
- await handlePainRecord({ reason: 'codex mistake', session: 'sess-codex', host: 'codex', json: true });
427
+ await handlePainRecord({
428
+ reason: 'codex mistake',
429
+ session: 'codex-root-sess',
430
+ host: 'codex',
431
+ rolloutId: 'rollout-abc',
432
+ hostTurnId: 'turn-7',
433
+ json: true,
434
+ });
435
+
436
+ expect(lastRecordPainInput).toBeTruthy();
437
+ expect(lastRecordPainInput!.hostKind).toBe('codex');
438
+ expect(lastRecordPainInput!.sessionId).toBe('codex-root-sess');
439
+ expect(lastRecordPainInput!.provenance).toBe('host_context_bound');
440
+ expect(lastRecordPainInput!.recordObservability).toBe(true);
441
+ // The CLI must NOT read the OpenClaw trajectory for a Codex attribution.
442
+ expect(acquireTrajectoryEvidenceFromDb).not.toHaveBeenCalled();
443
+ // Codex lineage is retained unflattened in the versioned payload (SPEC §8.1);
444
+ // the derived key mirrors the ingestion rule (codex|<rollout>|<turn>|user).
445
+ expect(lastRecordPainInput!.painIngress.correlation).toEqual({
446
+ status: 'bound',
447
+ hostKind: 'codex',
448
+ rootSessionId: 'codex-root-sess',
449
+ rolloutIdentity: 'rollout-abc',
450
+ logicalObservationKey: 'codex|rollout-abc|turn-7|user',
451
+ hostTurnId: 'turn-7',
452
+ });
453
+ // The degraded-but-honest path still submits (no fabricated evidence).
454
+ expect(lastRecordPainInput!.evidence).toEqual([]);
455
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
456
+
457
+ logSpy.mockRestore();
458
+ exitSpy.mockRestore();
459
+ });
460
+
461
+ it('PRI-743: --host codex without the Codex lineage fails loudly before any mutation', async () => {
462
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
463
+ const exitSpy = mockProcessExit();
464
+
465
+ await handlePainRecord({ reason: 'codex mistake', host: 'codex', json: true });
428
466
 
429
467
  // cli-1: exactly one JSON object on stdout
430
468
  expect(logSpy).toHaveBeenCalledTimes(1);
431
- const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]) as { status: string; reason: string; nextAction: string };
432
- expect(jsonOutput).toMatchObject({ status: 'failed', reason: 'codex_lineage_unverifiable_by_cli' });
433
- expect(jsonOutput.nextAction).toContain('Codex ingestion');
434
- // cli-2/cli-5: refused before any evidence acquisition or service mutation
469
+ const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]) as { status: string; reason: string; nextAction: string; message: string };
470
+ expect(jsonOutput).toMatchObject({ status: 'failed', reason: 'codex_lineage_required' });
471
+ expect(jsonOutput.message).toContain('--rollout-id');
472
+ expect(jsonOutput.nextAction).toContain('--host-turn-id');
473
+ // cli-2/cli-5: stopped before any evidence acquisition or service mutation
435
474
  expect(lastRecordPainInput).toBeNull();
436
475
  expect(acquireTrajectoryEvidenceFromDb).not.toHaveBeenCalled();
437
476
  expect(exitSpy).toHaveBeenCalledWith(1);
@@ -440,7 +479,27 @@ describe('pd pain record', () => {
440
479
  exitSpy.mockRestore();
441
480
  });
442
481
 
443
- it('PRI-743: omitting --host keeps the openclaw default but discloses the assumption (rc-9)', async () => {
482
+ it('PRI-743: --host codex with PARTIAL lineage still fails loudly (only the missing flag is reported)', async () => {
483
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
484
+ const exitSpy = mockProcessExit();
485
+
486
+ await handlePainRecord({ reason: 'codex mistake', session: 'codex-root', host: 'codex', rolloutId: 'rollout-abc', json: true });
487
+
488
+ const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]) as { status: string; reason: string; message: string };
489
+ expect(jsonOutput).toMatchObject({ status: 'failed', reason: 'codex_lineage_required' });
490
+ expect(jsonOutput.message).toContain('--host-turn-id');
491
+ expect(jsonOutput.message).not.toContain('--rollout-id');
492
+ expect(lastRecordPainInput).toBeNull();
493
+ expect(exitSpy).toHaveBeenCalledWith(1);
494
+
495
+ logSpy.mockRestore();
496
+ exitSpy.mockRestore();
497
+ });
498
+
499
+ // Task test #3/#4: legacy call compatibility — the pre-PRI-743 invocation
500
+ // (`--reason … --session …`, no --host) keeps host_kind=openclaw and only
501
+ // gains the disclosure wording.
502
+ it('PRI-743: legacy call omitting --host keeps the openclaw default but discloses it (rc-9)', async () => {
444
503
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
445
504
  const exitSpy = mockProcessExit();
446
505
 
@@ -450,7 +509,7 @@ describe('pd pain record', () => {
450
509
  expect(lastRecordPainInput!.hostKind).toBe('openclaw');
451
510
  const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]) as { hostAttribution?: string; warnings?: string[] };
452
511
  expect(jsonOutput.hostAttribution).toBe('openclaw');
453
- expect((jsonOutput.warnings ?? []).some((w) => w.includes("defaulted to 'openclaw'") && w.includes('--host codex'))).toBe(true);
512
+ expect((jsonOutput.warnings ?? []).some((w) => w.includes('host kind not specified, defaulting to openclaw') && w.includes('--host codex'))).toBe(true);
454
513
 
455
514
  logSpy.mockRestore();
456
515
  exitSpy.mockRestore();
@@ -2035,7 +2035,7 @@ describe('handleRuntimeActivationDispatch — PRI-634-F R2 host provenance refus
2035
2035
  });
2036
2036
 
2037
2037
  it('code_tool_hook dispatch threads the resolved registry into RuleHostWriter and gateDeps', async () => {
2038
- const registry = { version: 1, hasHostLayer: true, resolve: () => 'write', lookup: () => null, hasHostTool: () => true };
2038
+ const registry = { version: 1, hasHostLayer: true, resolve: () => 'write', lookup: () => null, hasHostTool: () => true, hostMappings: () => [{ rawToolName: 'write', canonicalKind: 'write' as const }] };
2039
2039
  vi.mocked(resolveWorkspaceToolSemantics).mockReturnValueOnce({ ok: true, registry, hostKind: 'openclaw' });
2040
2040
  await handleRuntimeActivationDispatch({
2041
2041
  workspace: WS,
@@ -86,6 +86,7 @@ vi.mock('@principles/core/runtime-v2', () => ({
86
86
  ArtificerRunner: vi.fn().mockImplementation(function () {
87
87
  return { run: mockRun };
88
88
  }),
89
+ buildArtificerHostSemanticContext: (registry) => ({ hostKinds: [], tools: registry.hostMappings() }),
89
90
  EvaluatorRunner: vi.fn().mockImplementation(function () {
90
91
  return { run: mockRun };
91
92
  }),
@@ -197,6 +198,10 @@ const { mockCreateEvaluatorRuntimeContext, mockCreateRolloutGovernanceDeps, mock
197
198
  const mockCreateEvaluatorRuntimeContext = vi.fn().mockReturnValue({
198
199
  ok: true,
199
200
  gateDeps: { evaluateInSandbox: vi.fn() },
201
+ // PRI-741: the context resolution also carries the single registry
202
+ // snapshot the parity projection derives from.
203
+ registry: { resolve: vi.fn(), hostMappings: vi.fn().mockReturnValue([{ rawToolName: 'write', canonicalKind: 'write' }]) },
204
+ hostKinds: ['openclaw'],
200
205
  });
201
206
  const mockCreateRolloutGovernanceDeps = vi.fn().mockReturnValue({
202
207
  dispatchActivation: vi.fn(),
@@ -204,7 +209,9 @@ const { mockCreateEvaluatorRuntimeContext, mockCreateRolloutGovernanceDeps, mock
204
209
  });
205
210
  const mockResolveWorkspaceHostToolSemantics = vi.fn().mockReturnValue({
206
211
  ok: true,
207
- registry: { resolve: vi.fn() },
212
+ // PRI-741: the projection reads the host layer via hostMappings() — the
213
+ // mock registry carries it like a real buildToolSemanticRegistry product.
214
+ registry: { resolve: vi.fn(), hostMappings: vi.fn().mockReturnValue([{ rawToolName: 'write', canonicalKind: 'write' }]) },
208
215
  hostKinds: ['openclaw'],
209
216
  });
210
217
  return { mockCreateEvaluatorRuntimeContext, mockCreateRolloutGovernanceDeps, mockResolveWorkspaceHostToolSemantics };