@principles/pd-cli 1.145.2 → 1.146.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/README.md +11 -2
- package/dist/commands/build-trajectory-evidence.d.ts +25 -6
- package/dist/commands/build-trajectory-evidence.d.ts.map +1 -1
- package/dist/commands/build-trajectory-evidence.js +174 -114
- package/dist/commands/build-trajectory-evidence.js.map +1 -1
- package/dist/commands/console.d.ts +13 -0
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +104 -0
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/pain-record.d.ts.map +1 -1
- package/dist/commands/pain-record.js +212 -17
- package/dist/commands/pain-record.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/build-trajectory-evidence.ts +211 -122
- package/src/commands/console.ts +105 -0
- package/src/commands/pain-record.ts +224 -16
- package/src/index.ts +1 -1
- package/tests/commands/build-trajectory-evidence.test.ts +226 -161
- package/tests/commands/console-open.test.ts +175 -0
- package/tests/commands/pain-record-async.test.ts +36 -30
- package/tests/commands/pain-record-session-parser.test.ts +127 -0
- package/tests/commands/pain-record.test.ts +205 -37
|
@@ -21,21 +21,31 @@ vi.mock('fs', () => ({
|
|
|
21
21
|
}));
|
|
22
22
|
|
|
23
23
|
vi.mock('../../src/commands/build-trajectory-evidence.js', () => ({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
acquireTrajectoryEvidenceFromDb: vi.fn().mockReturnValue({
|
|
25
|
+
status: 'available',
|
|
26
|
+
entries: [
|
|
27
|
+
{ sourceRef: 'owner_message:2026-01-01T10:00:00Z', note: 'Owner correction' },
|
|
28
|
+
{ sourceRef: 'agent_turn:2026-01-01T10:01:00Z', note: 'assistant evidence' },
|
|
29
|
+
],
|
|
30
|
+
}),
|
|
27
31
|
}));
|
|
28
32
|
|
|
29
|
-
vi.mock('@principles/core/runtime-v2', () =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
vi.mock('@principles/core/runtime-v2', async (importOriginal) => {
|
|
34
|
+
// PRI-642 review blocker 1: the CLI now evaluates ingress semantics via
|
|
35
|
+
// the REAL core evaluatePainIngress (the shared authority) — only the
|
|
36
|
+
// service/IO classes are mocked.
|
|
37
|
+
const actual = await importOriginal<typeof import('@principles/core/runtime-v2')>();
|
|
38
|
+
return {
|
|
39
|
+
...actual,
|
|
40
|
+
PainToPrincipleService: vi.fn().mockImplementation(function() {
|
|
41
|
+
return {
|
|
42
|
+
recordPain: vi.fn(async (input: PainToPrincipleInput) => {
|
|
43
|
+
lastRecordPainInput = input;
|
|
44
|
+
return mockRecordPainResult;
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
}),
|
|
48
|
+
PrincipleTreeLedgerAdapter: vi.fn().mockImplementation(function() { return {}; }),
|
|
39
49
|
computeEffectivePdConfig: vi.fn().mockReturnValue({
|
|
40
50
|
runtimeKind: 'pi-ai',
|
|
41
51
|
provider: 'test-provider',
|
|
@@ -58,7 +68,8 @@ vi.mock('@principles/core/runtime-v2', () => ({
|
|
|
58
68
|
isBuiltinPiAiProvider: vi.fn().mockReturnValue(true),
|
|
59
69
|
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
60
70
|
isFeatureEnabled: vi.fn().mockReturnValue(false),
|
|
61
|
-
}
|
|
71
|
+
};
|
|
72
|
+
});
|
|
62
73
|
|
|
63
74
|
vi.mock('../../src/services/pd-config-loader.js', () => ({
|
|
64
75
|
loadPdConfig: vi.fn().mockReturnValue({
|
|
@@ -77,8 +88,9 @@ vi.mock('../../src/services/pd-config-loader.js', () => ({
|
|
|
77
88
|
}));
|
|
78
89
|
|
|
79
90
|
import { handlePainRecord } from '../../src/commands/pain-record.js';
|
|
80
|
-
import { isBuiltinPiAiProvider } from '@principles/core/runtime-v2';
|
|
81
|
-
import type {
|
|
91
|
+
import { isBuiltinPiAiProvider, type PainToPrincipleOutput, type PainToPrincipleInput } from '@principles/core/runtime-v2';
|
|
92
|
+
import type { FailureCategory } from '@principles/core/runtime-v2';
|
|
93
|
+
import { acquireTrajectoryEvidenceFromDb } from '../../src/commands/build-trajectory-evidence.js';
|
|
82
94
|
|
|
83
95
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
84
96
|
|
|
@@ -351,51 +363,207 @@ describe('pd pain record', () => {
|
|
|
351
363
|
expect(lastRecordPainInput!.source).toBe('ci');
|
|
352
364
|
expect(lastRecordPainInput!.reason).toBe('test pain');
|
|
353
365
|
expect(lastRecordPainInput!.score).toBe(90);
|
|
354
|
-
|
|
366
|
+
// PRI-642: no --session means an explicit unbound Owner report — the
|
|
367
|
+
// 'cli' sentinel session must NOT be fabricated (SPEC §7.4).
|
|
368
|
+
expect(lastRecordPainInput!.sessionId).toBeUndefined();
|
|
355
369
|
expect(lastRecordPainInput!.agentId).toBe('pd-cli');
|
|
356
|
-
//
|
|
357
|
-
expect(lastRecordPainInput!.evidence).
|
|
358
|
-
expect(lastRecordPainInput!.
|
|
370
|
+
// …and no placeholder evidence entry may be submitted.
|
|
371
|
+
expect(lastRecordPainInput!.evidence).toEqual([]);
|
|
372
|
+
expect(lastRecordPainInput!.recordObservability).toBe(false);
|
|
373
|
+
expect(lastRecordPainInput!.provenance).toBe('owner_reported_no_host_trace');
|
|
359
374
|
});
|
|
360
375
|
|
|
361
|
-
// ── PRI-341: evidence passthrough and --session flag
|
|
362
|
-
|
|
363
|
-
// 用例 C: recordPain receives non-empty evidence field when session provided
|
|
364
|
-
it('C: passes evidence to recordPain when --session is provided', async () => {
|
|
365
|
-
// Mock buildTrajectoryEvidenceFromDb to return evidence
|
|
366
|
-
const mockEvidence = [
|
|
367
|
-
{ sourceRef: 'agent_turn:2026-01-01T10:00:00Z', note: 'assistant text evidence' },
|
|
368
|
-
];
|
|
369
|
-
vi.doMock('../../src/commands/build-trajectory-evidence.js', () => ({
|
|
370
|
-
buildTrajectoryEvidenceFromDb: vi.fn().mockReturnValue(mockEvidence),
|
|
371
|
-
}));
|
|
376
|
+
// ── PRI-341 → PRI-642: evidence passthrough and --session flag ─────────────
|
|
372
377
|
|
|
378
|
+
// 用例 C: recordPain receives bound provenance + validated evidence when a real session is provided
|
|
379
|
+
it('C: passes bound provenance and evidence to recordPain when --session resolves available', async () => {
|
|
373
380
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
374
381
|
const exitSpy = mockProcessExit();
|
|
375
382
|
|
|
376
383
|
await handlePainRecord({ reason: 'test pain', session: 'sess-123', json: true });
|
|
377
384
|
|
|
378
385
|
expect(lastRecordPainInput).toBeTruthy();
|
|
386
|
+
expect(acquireTrajectoryEvidenceFromDb).toHaveBeenCalled();
|
|
379
387
|
expect(lastRecordPainInput!.evidence).toBeTruthy();
|
|
380
388
|
expect(lastRecordPainInput!.evidence!.length).toBeGreaterThan(0);
|
|
389
|
+
expect(lastRecordPainInput!.evidence![0].sourceRef).not.toBe('owner_reported:cli');
|
|
381
390
|
expect(lastRecordPainInput!.sessionId).toBe('sess-123');
|
|
391
|
+
// SPEC §7.3: provenance must be derived from the validated result —
|
|
392
|
+
// a real session is host-context-bound, never owner_reported_no_host_trace.
|
|
393
|
+
expect(lastRecordPainInput!.provenance).toBe('host_context_bound');
|
|
394
|
+
// PRI-640/SPEC §8.3: the bound path attributes the OpenClaw trajectory.
|
|
395
|
+
expect(lastRecordPainInput!.hostKind).toBe('openclaw');
|
|
396
|
+
expect(lastRecordPainInput!.recordObservability).toBe(true);
|
|
382
397
|
|
|
383
398
|
logSpy.mockRestore();
|
|
384
399
|
exitSpy.mockRestore();
|
|
385
400
|
});
|
|
386
401
|
|
|
387
|
-
// 用例 C2: without session,
|
|
388
|
-
|
|
402
|
+
// 用例 C2 (PRI-642 rewrite): without session, no sentinel session, no
|
|
403
|
+
// placeholder evidence — an honest unbound Owner report (SPEC §7.4).
|
|
404
|
+
it('C2: submits an honest unbound report when no --session provided', async () => {
|
|
389
405
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
390
406
|
const exitSpy = mockProcessExit();
|
|
391
407
|
|
|
392
408
|
await handlePainRecord({ reason: 'test pain', json: true });
|
|
393
409
|
|
|
394
410
|
expect(lastRecordPainInput).toBeTruthy();
|
|
395
|
-
expect(lastRecordPainInput!.
|
|
396
|
-
expect(lastRecordPainInput!.evidence
|
|
397
|
-
|
|
398
|
-
expect(lastRecordPainInput!.
|
|
411
|
+
expect(lastRecordPainInput!.sessionId).toBeUndefined();
|
|
412
|
+
expect(lastRecordPainInput!.evidence).toEqual([]);
|
|
413
|
+
expect(lastRecordPainInput!.recordObservability).toBe(false);
|
|
414
|
+
expect(lastRecordPainInput!.provenance).toBe('owner_reported_no_host_trace');
|
|
415
|
+
|
|
416
|
+
const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]);
|
|
417
|
+
// SPEC §7.4: disclose context_unbound and recommend --session.
|
|
418
|
+
const warnings = JSON.stringify(jsonOutput);
|
|
419
|
+
expect(warnings).toMatch(/context_unbound/);
|
|
420
|
+
expect(warnings).toMatch(/--session/);
|
|
421
|
+
|
|
422
|
+
logSpy.mockRestore();
|
|
423
|
+
exitSpy.mockRestore();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// ── PRI-642 Scope A: explicit-session validation (SPEC §7.3, §12.1.3–12.1.4) ─
|
|
427
|
+
|
|
428
|
+
it('fails with session_not_found before any task/candidate mutation when --session does not exist', async () => {
|
|
429
|
+
vi.mocked(acquireTrajectoryEvidenceFromDb).mockReturnValueOnce({
|
|
430
|
+
status: 'unavailable',
|
|
431
|
+
reasonCode: 'session_not_found',
|
|
432
|
+
detail: 'session not present in trajectory.db sessions table',
|
|
433
|
+
} as any);
|
|
434
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
435
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
436
|
+
const exitSpy = mockProcessExit();
|
|
437
|
+
|
|
438
|
+
await handlePainRecord({ reason: 'test pain', session: 'missing-session', json: true });
|
|
439
|
+
|
|
440
|
+
// cli-1: exactly one JSON object on stdout
|
|
441
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
442
|
+
const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]);
|
|
443
|
+
expect(jsonOutput.status).toBe('failed');
|
|
444
|
+
expect(jsonOutput.reason).toBe('session_not_found');
|
|
445
|
+
expect(jsonOutput.nextAction).toBeTruthy();
|
|
446
|
+
// cli-2/cli-5: execution stopped — no service mutation, exit 1
|
|
447
|
+
expect(lastRecordPainInput).toBeNull();
|
|
448
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
449
|
+
|
|
450
|
+
logSpy.mockRestore();
|
|
451
|
+
errorSpy.mockRestore();
|
|
452
|
+
exitSpy.mockRestore();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('fails before any mutation on session_not_found even when process.exit is stubbed to a no-op', async () => {
|
|
456
|
+
vi.mocked(acquireTrajectoryEvidenceFromDb).mockReturnValueOnce({
|
|
457
|
+
status: 'unavailable',
|
|
458
|
+
reasonCode: 'session_not_found',
|
|
459
|
+
detail: 'session not present',
|
|
460
|
+
} as any);
|
|
461
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
462
|
+
const exitSpy = mockProcessExit();
|
|
463
|
+
|
|
464
|
+
await handlePainRecord({ reason: 'test pain', session: 'missing-session', json: true });
|
|
465
|
+
|
|
466
|
+
// cli-2: with process.exit stubbed, control flow must still stop —
|
|
467
|
+
// recordPain was never invoked and stdout carries exactly one object.
|
|
468
|
+
expect(lastRecordPainInput).toBeNull();
|
|
469
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
470
|
+
logSpy.mockRestore();
|
|
471
|
+
exitSpy.mockRestore();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('fails before mutation on empty_trajectory (CLI never claims host_context_bound without a verified session)', async () => {
|
|
475
|
+
// Per Evidence Over Assumption: the CLI does not own session identity
|
|
476
|
+
// the way the OpenClaw host command context does. Unverified sessions
|
|
477
|
+
// (any acquisition that does not yield 'available') must refuse before
|
|
478
|
+
// any LLM/task/candidate mutation.
|
|
479
|
+
vi.mocked(acquireTrajectoryEvidenceFromDb).mockReturnValueOnce({
|
|
480
|
+
status: 'unavailable',
|
|
481
|
+
reasonCode: 'empty_trajectory',
|
|
482
|
+
detail: 'session exists but no usable evidence',
|
|
483
|
+
} as any);
|
|
484
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
485
|
+
const exitSpy = mockProcessExit();
|
|
486
|
+
|
|
487
|
+
await handlePainRecord({ reason: 'test pain', session: 'quiet-session', json: true });
|
|
488
|
+
|
|
489
|
+
expect(lastRecordPainInput).toBeNull();
|
|
490
|
+
const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]);
|
|
491
|
+
expect(jsonOutput.status).toBe('failed');
|
|
492
|
+
expect(jsonOutput.reason).toBe('empty_trajectory');
|
|
493
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
494
|
+
|
|
495
|
+
logSpy.mockRestore();
|
|
496
|
+
exitSpy.mockRestore();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('fails before mutation on evidence_read_failed (no LLM/no task when the CLI cannot verify binding)', async () => {
|
|
500
|
+
vi.mocked(acquireTrajectoryEvidenceFromDb).mockReturnValueOnce({
|
|
501
|
+
status: 'unavailable',
|
|
502
|
+
reasonCode: 'evidence_read_failed',
|
|
503
|
+
detail: 'trajectory.db unreadable',
|
|
504
|
+
} as any);
|
|
505
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
506
|
+
const exitSpy = mockProcessExit();
|
|
507
|
+
|
|
508
|
+
await handlePainRecord({ reason: 'test pain', session: 'sess-x', json: true });
|
|
509
|
+
|
|
510
|
+
expect(lastRecordPainInput).toBeNull();
|
|
511
|
+
const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]);
|
|
512
|
+
expect(jsonOutput.status).toBe('failed');
|
|
513
|
+
expect(jsonOutput.reason).toBe('evidence_read_failed');
|
|
514
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
515
|
+
|
|
516
|
+
logSpy.mockRestore();
|
|
517
|
+
exitSpy.mockRestore();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('fails before mutation on trajectory_unavailable (CLI cannot fabricate a session)', async () => {
|
|
521
|
+
vi.mocked(acquireTrajectoryEvidenceFromDb).mockReturnValueOnce({
|
|
522
|
+
status: 'unavailable',
|
|
523
|
+
reasonCode: 'trajectory_unavailable',
|
|
524
|
+
detail: 'no trajectory.db at workspace .state',
|
|
525
|
+
} as any);
|
|
526
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
527
|
+
const exitSpy = mockProcessExit();
|
|
528
|
+
|
|
529
|
+
await handlePainRecord({ reason: 'test pain', session: 'sess-x', json: true });
|
|
530
|
+
|
|
531
|
+
expect(lastRecordPainInput).toBeNull();
|
|
532
|
+
const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]);
|
|
533
|
+
expect(jsonOutput.status).toBe('failed');
|
|
534
|
+
expect(jsonOutput.reason).toBe('trajectory_unavailable');
|
|
535
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
536
|
+
|
|
537
|
+
logSpy.mockRestore();
|
|
538
|
+
exitSpy.mockRestore();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// ── PRI-642 Scope A (SPEC §12.1.6): generated-but-unadmitted candidates must
|
|
542
|
+
// not be reported as completed internalization. ─────────────────────────────
|
|
543
|
+
|
|
544
|
+
it('warns that no candidate was admitted/internalized when all candidates are gated', async () => {
|
|
545
|
+
mockRecordPainResult = {
|
|
546
|
+
...SUCCEEDED_RESULT,
|
|
547
|
+
candidateIds: ['c1', 'c2', 'c3', 'c4'],
|
|
548
|
+
ledgerEntryIds: [],
|
|
549
|
+
admissionResults: [
|
|
550
|
+
{ candidateId: 'c1', recommendationKind: 'prompt', admission: { decision: 'needs_evidence', reason: 'confidence_below_threshold:0.45<0.50', nextAction: 'add evidence', evidenceStatus: 'host_context_bound' } },
|
|
551
|
+
{ candidateId: 'c2', recommendationKind: 'prompt', admission: { decision: 'deferred', reason: 'recommendation_kind_defer_not_actionable', nextAction: 'none', evidenceStatus: 'host_context_bound' } },
|
|
552
|
+
{ candidateId: 'c3', recommendationKind: 'prompt', admission: { decision: 'needs_evidence', reason: 'input_evidence_empty', nextAction: 'add evidence', evidenceStatus: 'host_context_bound' } },
|
|
553
|
+
{ candidateId: 'c4', recommendationKind: 'prompt', admission: { decision: 'needs_evidence', reason: 'confidence_below_threshold:0.45<0.50', nextAction: 'add evidence', evidenceStatus: 'host_context_bound' } },
|
|
554
|
+
],
|
|
555
|
+
} as PainToPrincipleOutput;
|
|
556
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
557
|
+
const exitSpy = mockProcessExit();
|
|
558
|
+
|
|
559
|
+
await handlePainRecord({ reason: 'test pain', json: true });
|
|
560
|
+
|
|
561
|
+
const allOutput = logSpy.mock.calls.map(c => c.join(' ')).join(' ');
|
|
562
|
+
// Explicit admission summary; must not read as completed internalization.
|
|
563
|
+
expect(allOutput).toMatch(/0 (?:of|\/) 4|admitted.*0|0.*admitted/i);
|
|
564
|
+
const jsonOutput = JSON.parse(logSpy.mock.calls.at(-1)![0] ?? '{}');
|
|
565
|
+
const jsonStr = JSON.stringify(jsonOutput);
|
|
566
|
+
expect(jsonStr).toMatch(/no candidate was admitted|admitted:\s*0/i);
|
|
399
567
|
|
|
400
568
|
logSpy.mockRestore();
|
|
401
569
|
exitSpy.mockRestore();
|