@principles/pd-cli 1.133.0 → 1.135.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.
Files changed (215) hide show
  1. package/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
  2. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
  3. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +2 -0
  4. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +1 -0
  5. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +60 -0
  6. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +1 -0
  7. package/dist/commands/__tests__/rulecode-flag-wiring.test.js +11 -4
  8. package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -1
  9. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +9 -0
  10. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -1
  11. package/dist/commands/admission-gate.d.ts +24 -0
  12. package/dist/commands/admission-gate.d.ts.map +1 -0
  13. package/dist/commands/admission-gate.js +29 -0
  14. package/dist/commands/admission-gate.js.map +1 -0
  15. package/dist/commands/candidate.d.ts.map +1 -1
  16. package/dist/commands/candidate.js +72 -6
  17. package/dist/commands/candidate.js.map +1 -1
  18. package/dist/commands/console.d.ts.map +1 -1
  19. package/dist/commands/console.js +42 -0
  20. package/dist/commands/console.js.map +1 -1
  21. package/dist/commands/demo-story-a.d.ts.map +1 -1
  22. package/dist/commands/demo-story-a.js +4 -2
  23. package/dist/commands/demo-story-a.js.map +1 -1
  24. package/dist/commands/diagnose.d.ts.map +1 -1
  25. package/dist/commands/diagnose.js +155 -8
  26. package/dist/commands/diagnose.js.map +1 -1
  27. package/dist/commands/errors-list.d.ts +25 -0
  28. package/dist/commands/errors-list.d.ts.map +1 -0
  29. package/dist/commands/errors-list.js +368 -0
  30. package/dist/commands/errors-list.js.map +1 -0
  31. package/dist/commands/health-codex.d.ts +7 -0
  32. package/dist/commands/health-codex.d.ts.map +1 -0
  33. package/dist/commands/health-codex.js +209 -0
  34. package/dist/commands/health-codex.js.map +1 -0
  35. package/dist/commands/health.d.ts +24 -0
  36. package/dist/commands/health.d.ts.map +1 -1
  37. package/dist/commands/health.js +48 -0
  38. package/dist/commands/health.js.map +1 -1
  39. package/dist/commands/intent.d.ts +21 -4
  40. package/dist/commands/intent.d.ts.map +1 -1
  41. package/dist/commands/intent.js +88 -27
  42. package/dist/commands/intent.js.map +1 -1
  43. package/dist/commands/pain-evidence.d.ts +6 -0
  44. package/dist/commands/pain-evidence.d.ts.map +1 -1
  45. package/dist/commands/pain-evidence.js +6 -0
  46. package/dist/commands/pain-evidence.js.map +1 -1
  47. package/dist/commands/pain-record.d.ts.map +1 -1
  48. package/dist/commands/pain-record.js +21 -0
  49. package/dist/commands/pain-record.js.map +1 -1
  50. package/dist/commands/pain-retry.d.ts +4 -0
  51. package/dist/commands/pain-retry.d.ts.map +1 -1
  52. package/dist/commands/pain-retry.js +327 -32
  53. package/dist/commands/pain-retry.js.map +1 -1
  54. package/dist/commands/proven-channel-baseline.d.ts.map +1 -1
  55. package/dist/commands/proven-channel-baseline.js +4 -2
  56. package/dist/commands/proven-channel-baseline.js.map +1 -1
  57. package/dist/commands/runtime-activation.d.ts +30 -0
  58. package/dist/commands/runtime-activation.d.ts.map +1 -1
  59. package/dist/commands/runtime-activation.js +567 -11
  60. package/dist/commands/runtime-activation.js.map +1 -1
  61. package/dist/commands/runtime-canary.d.ts.map +1 -1
  62. package/dist/commands/runtime-canary.js +32 -7
  63. package/dist/commands/runtime-canary.js.map +1 -1
  64. package/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
  65. package/dist/commands/runtime-health-snapshot.js +4 -2
  66. package/dist/commands/runtime-health-snapshot.js.map +1 -1
  67. package/dist/commands/runtime-init.d.ts +43 -0
  68. package/dist/commands/runtime-init.d.ts.map +1 -0
  69. package/dist/commands/runtime-init.js +264 -0
  70. package/dist/commands/runtime-init.js.map +1 -0
  71. package/dist/commands/runtime-internalization-context-trace.d.ts +25 -0
  72. package/dist/commands/runtime-internalization-context-trace.d.ts.map +1 -0
  73. package/dist/commands/runtime-internalization-context-trace.js +374 -0
  74. package/dist/commands/runtime-internalization-context-trace.js.map +1 -0
  75. package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
  76. package/dist/commands/runtime-internalization-enqueue-successors.js +3 -1
  77. package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
  78. package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
  79. package/dist/commands/runtime-internalization-integrity.js +4 -2
  80. package/dist/commands/runtime-internalization-integrity.js.map +1 -1
  81. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  82. package/dist/commands/runtime-internalization-run-once.js +20 -5
  83. package/dist/commands/runtime-internalization-run-once.js.map +1 -1
  84. package/dist/commands/runtime-internalization-run-rulehost.d.ts +1 -0
  85. package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
  86. package/dist/commands/runtime-internalization-run-rulehost.js +149 -6
  87. package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
  88. package/dist/commands/runtime-uat.d.ts.map +1 -1
  89. package/dist/commands/runtime-uat.guard.test.js +13 -15
  90. package/dist/commands/runtime-uat.guard.test.js.map +1 -1
  91. package/dist/commands/runtime-uat.js +4 -3
  92. package/dist/commands/runtime-uat.js.map +1 -1
  93. package/dist/config-reader.js +1 -1
  94. package/dist/config-reader.js.map +1 -1
  95. package/dist/index.js +54 -78
  96. package/dist/index.js.map +1 -1
  97. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +2 -0
  98. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +1 -0
  99. package/dist/services/__tests__/evaluator-runner-deps.test.js +304 -0
  100. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -0
  101. package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
  102. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
  103. package/dist/services/config-doctor.d.ts.map +1 -1
  104. package/dist/services/config-doctor.js +9 -2
  105. package/dist/services/config-doctor.js.map +1 -1
  106. package/dist/services/console-launcher.d.ts +6 -0
  107. package/dist/services/console-launcher.d.ts.map +1 -1
  108. package/dist/services/console-launcher.js.map +1 -1
  109. package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
  110. package/dist/services/pain-flood-simulation-runner.js +9 -0
  111. package/dist/services/pain-flood-simulation-runner.js.map +1 -1
  112. package/dist/services/pd-config-loader.js +2 -2
  113. package/dist/services/pd-config-loader.js.map +1 -1
  114. package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
  115. package/dist/services/proven-channel-baseline-runner.js +10 -2
  116. package/dist/services/proven-channel-baseline-runner.js.map +1 -1
  117. package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
  118. package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
  119. package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
  120. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  121. package/dist/services/rulehost-pipeline-runner.js +111 -7
  122. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  123. package/dist/services/rulehost-readiness.js.map +1 -1
  124. package/dist/services/runtime-adapter-resolver.d.ts +1 -0
  125. package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
  126. package/dist/services/runtime-adapter-resolver.js +3 -0
  127. package/dist/services/runtime-adapter-resolver.js.map +1 -1
  128. package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
  129. package/dist/services/synthetic-baseline-runner.js +11 -0
  130. package/dist/services/synthetic-baseline-runner.js.map +1 -1
  131. package/dist/utils/production-workspace-guard.d.ts +4 -4
  132. package/dist/utils/production-workspace-guard.d.ts.map +1 -1
  133. package/dist/utils/production-workspace-guard.js +19 -16
  134. package/dist/utils/production-workspace-guard.js.map +1 -1
  135. package/dist/utils/production-workspace-guard.test.js +54 -19
  136. package/dist/utils/production-workspace-guard.test.js.map +1 -1
  137. package/package.json +8 -5
  138. package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
  139. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
  140. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
  141. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
  142. package/src/commands/admission-gate.ts +30 -0
  143. package/src/commands/candidate.ts +74 -6
  144. package/src/commands/console.ts +41 -0
  145. package/src/commands/demo-story-a.ts +4 -2
  146. package/src/commands/diagnose.ts +158 -10
  147. package/src/commands/errors-list.ts +440 -0
  148. package/src/commands/health-codex.ts +249 -0
  149. package/src/commands/health.ts +60 -0
  150. package/src/commands/intent.ts +98 -29
  151. package/src/commands/pain-evidence.ts +6 -0
  152. package/src/commands/pain-record.ts +21 -0
  153. package/src/commands/pain-retry.ts +353 -35
  154. package/src/commands/proven-channel-baseline.ts +4 -2
  155. package/src/commands/runtime-activation.ts +657 -12
  156. package/src/commands/runtime-canary.ts +31 -5
  157. package/src/commands/runtime-health-snapshot.ts +4 -2
  158. package/src/commands/runtime-init.ts +305 -0
  159. package/src/commands/runtime-internalization-context-trace.ts +445 -0
  160. package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
  161. package/src/commands/runtime-internalization-integrity.ts +4 -2
  162. package/src/commands/runtime-internalization-run-once.ts +20 -5
  163. package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
  164. package/src/commands/runtime-uat.guard.test.ts +14 -16
  165. package/src/commands/runtime-uat.ts +4 -3
  166. package/src/config-reader.ts +1 -1
  167. package/src/index.ts +69 -78
  168. package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
  169. package/src/services/config-doctor.ts +9 -2
  170. package/src/services/console-launcher.ts +6 -0
  171. package/src/services/pain-flood-simulation-runner.ts +9 -0
  172. package/src/services/pd-config-loader.ts +2 -2
  173. package/src/services/proven-channel-baseline-runner.ts +10 -3
  174. package/src/services/rulehost-pipeline-runner.ts +157 -6
  175. package/src/services/runtime-adapter-resolver.ts +4 -0
  176. package/src/services/synthetic-baseline-runner.ts +11 -0
  177. package/src/utils/production-workspace-guard.test.ts +62 -20
  178. package/src/utils/production-workspace-guard.ts +19 -16
  179. package/tests/bdd/cli-contract.steps.ts +343 -0
  180. package/tests/bdd/context-trace.steps.ts +229 -0
  181. package/tests/bdd/support/gherkin-loader.ts +147 -0
  182. package/tests/bdd/support/repo-root.ts +62 -0
  183. package/tests/bdd/support/vitest-bdd.ts +179 -0
  184. package/tests/commands/admission-gate-coverage.test.ts +79 -0
  185. package/tests/commands/candidate-audit-repair.test.ts +77 -0
  186. package/tests/commands/candidate-intake.test.ts +155 -10
  187. package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
  188. package/tests/commands/cli-help-snapshot.test.ts +15 -1
  189. package/tests/commands/console-open.test.ts +229 -32
  190. package/tests/commands/diagnose.test.ts +619 -14
  191. package/tests/commands/errors-list.test.ts +678 -0
  192. package/tests/commands/health-codex.test.ts +243 -0
  193. package/tests/commands/health-host-wiring.test.ts +76 -0
  194. package/tests/commands/intent.test.ts +4 -4
  195. package/tests/commands/pain-record.test.ts +80 -0
  196. package/tests/commands/pain-retry.test.ts +94 -3
  197. package/tests/commands/run-rulehost-handler.test.ts +133 -0
  198. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
  199. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
  200. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
  201. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
  202. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
  203. package/tests/commands/runtime-activation.test.ts +1090 -5
  204. package/tests/commands/runtime-canary.test.ts +50 -0
  205. package/tests/commands/runtime-features.test.ts +2 -0
  206. package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
  207. package/tests/commands/runtime-init.test.ts +346 -0
  208. package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
  209. package/tests/commands/runtime-internalization.test.ts +2 -2
  210. package/tests/config-reader.test.ts +6 -5
  211. package/tests/e2e/cross-package-acceptance.test.ts +55 -13
  212. package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
  213. package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
  214. package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
  215. package/vitest.config.ts +1 -1
@@ -72,6 +72,19 @@ vi.mock('@principles/core/runtime-v2', () => ({
72
72
  RuntimeStateManager: MockRuntimeStateManager,
73
73
  resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
74
74
  PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
75
+ evaluateCandidateAdmissionFromRecord: vi.fn((candidate: { recommendationKind: string; confidence: number | null }) => {
76
+ // Mirror the real implementation for test purposes
77
+ if (candidate.recommendationKind === 'defer') {
78
+ return { decision: 'deferred', reason: 'recommendation_kind_defer_not_actionable', nextAction: 'review_defer_disposition_manually', evidenceStatus: 'unknown' };
79
+ }
80
+ if (candidate.confidence === null) {
81
+ return { decision: 'needs_evidence', reason: 'confidence_missing_on_candidate_record', nextAction: 're_run_diagnosis_to_populate_confidence_or_manual_review', evidenceStatus: 'unknown' };
82
+ }
83
+ if (candidate.confidence < 0.5) {
84
+ return { decision: 'needs_evidence', reason: `confidence_below_threshold:${candidate.confidence.toFixed(2)}<0.5`, nextAction: 'provide_additional_evidence_or_manual_review', evidenceStatus: 'unknown' };
85
+ }
86
+ return { decision: 'admitted', reason: 'cli_partial_check_passed_confidence_and_kind', nextAction: 'none', evidenceStatus: 'unknown' };
87
+ }),
75
88
  }));
76
89
 
77
90
  vi.mock('../../src/resolve-workspace.js', () => ({
@@ -348,29 +361,37 @@ describe('pd candidate intake', () => {
348
361
 
349
362
  // Test 4: Non-existent candidate
350
363
  it('Test 4 (non-existent candidate): throws CANDIDATE_NOT_FOUND error', async () => {
351
- // Setup: service.intake throws CANDIDATE_NOT_FOUND when candidate doesn't exist
352
- mockService.intake.mockImplementation(() => {
353
- throw new MockCandidateIntakeError(
354
- `Candidate invalid-id not found`,
355
- 'CANDIDATE_NOT_FOUND',
356
- { candidateId: 'invalid-id' }
357
- );
358
- });
364
+ // PRI-442 Stage 4: getCandidate now returns null before service.intake is called.
365
+ // The CLI exits with "Candidate not found" instead of relying on service.intake
366
+ // to throw CANDIDATE_NOT_FOUND.
367
+ mockStateManager.getCandidate.mockResolvedValue(null);
359
368
 
360
369
  await handleCandidateIntake({
361
370
  candidateId: 'invalid-id',
362
371
  workspace: '/tmp/test-workspace',
363
372
  });
364
373
 
365
- // Verify error output contains CANDIDATE_NOT_FOUND
374
+ // Verify error output contains "Candidate not found"
366
375
  expect(consoleErrorSpy).toHaveBeenCalledWith(
367
- expect.stringContaining('CANDIDATE_NOT_FOUND')
376
+ expect.stringContaining('Candidate not found')
368
377
  );
369
378
  expect(exitSpy).toHaveBeenCalledWith(1);
370
379
  });
371
380
 
372
381
  // Test 5: Invalid candidateId (empty string)
373
382
  it('Test 5 (invalid candidateId): empty string throws INPUT_INVALID error', async () => {
383
+ // PRI-442 Stage 4: getCandidate is called before service.intake, so we need
384
+ // to mock it to return a valid candidate so service.intake is reached.
385
+ const mockCandidate = {
386
+ candidateId: '',
387
+ artifactId: 'artifact-1',
388
+ taskId: 'task-1',
389
+ title: 'Test',
390
+ description: 'Test',
391
+ status: 'pending',
392
+ };
393
+ mockStateManager.getCandidate.mockResolvedValue(mockCandidate);
394
+
374
395
  // Setup: service.intake throws INPUT_INVALID for empty candidateId
375
396
  mockService.intake.mockImplementation(() => {
376
397
  throw new MockCandidateIntakeError(
@@ -475,6 +496,18 @@ describe('pd candidate intake', () => {
475
496
 
476
497
  // Test 7: Generic error (non-CandidateIntakeError, covers line 247)
477
498
  it('Test 7 (generic error): handles non-CandidateIntakeError errors', async () => {
499
+ // PRI-442 Stage 4: getCandidate is called before service.intake, so we need
500
+ // to mock it to return a valid candidate so service.intake is reached.
501
+ const mockCandidate = {
502
+ candidateId: 'valid-id',
503
+ artifactId: 'artifact-1',
504
+ taskId: 'task-1',
505
+ title: 'Test',
506
+ description: 'Test',
507
+ status: 'pending',
508
+ };
509
+ mockStateManager.getCandidate.mockResolvedValue(mockCandidate);
510
+
478
511
  // Setup: service.intake throws a generic Error (not CandidateIntakeError)
479
512
  mockService.intake.mockImplementation(() => {
480
513
  throw new Error('Some unexpected error');
@@ -584,4 +617,116 @@ describe('pd candidate intake', () => {
584
617
  mockStateManager.connection = originalConnection;
585
618
  }
586
619
  });
620
+
621
+ // PRI-442 Stage 4: admission gate bypass tests
622
+ describe('admission gate bypass prevention (PRI-442 Stage 4)', () => {
623
+ it('refuses intake when recommendationKind is defer', async () => {
624
+ const mockCandidate = {
625
+ candidateId: 'defer-cand',
626
+ artifactId: 'artifact-1',
627
+ taskId: 'task-1',
628
+ title: 'Deferred candidate',
629
+ description: 'Should not be intakeable',
630
+ status: 'pending',
631
+ recommendationKind: 'defer',
632
+ confidence: 0.9,
633
+ };
634
+ mockStateManager.getCandidate.mockResolvedValue(mockCandidate);
635
+
636
+ await handleCandidateIntake({
637
+ candidateId: 'defer-cand',
638
+ workspace: '/tmp/test-workspace',
639
+ json: true,
640
+ });
641
+
642
+ // Should NOT call service.intake
643
+ expect(mockService.intake).not.toHaveBeenCalled();
644
+ // Should output refused status
645
+ expect(consoleLogSpy).toHaveBeenCalledWith(
646
+ expect.stringContaining('"status": "refused"')
647
+ );
648
+ expect(consoleLogSpy).toHaveBeenCalledWith(
649
+ expect.stringContaining('"admissionDecision": "deferred"')
650
+ );
651
+ expect(exitSpy).toHaveBeenCalledWith(1);
652
+ });
653
+
654
+ it('refuses intake when confidence is below threshold', async () => {
655
+ const mockCandidate = {
656
+ candidateId: 'low-conf-cand',
657
+ artifactId: 'artifact-1',
658
+ taskId: 'task-1',
659
+ title: 'Low confidence candidate',
660
+ description: 'Should not be intakeable',
661
+ status: 'pending',
662
+ recommendationKind: 'principle',
663
+ confidence: 0.3,
664
+ };
665
+ mockStateManager.getCandidate.mockResolvedValue(mockCandidate);
666
+
667
+ await handleCandidateIntake({
668
+ candidateId: 'low-conf-cand',
669
+ workspace: '/tmp/test-workspace',
670
+ json: false,
671
+ });
672
+
673
+ expect(mockService.intake).not.toHaveBeenCalled();
674
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
675
+ expect.stringContaining('Admission gate refused')
676
+ );
677
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
678
+ expect.stringContaining('needs_evidence')
679
+ );
680
+ expect(exitSpy).toHaveBeenCalledWith(1);
681
+ });
682
+
683
+ it('refuses intake when confidence is null (rc-3 fail loud)', async () => {
684
+ const mockCandidate = {
685
+ candidateId: 'null-conf-cand',
686
+ artifactId: 'artifact-1',
687
+ taskId: 'task-1',
688
+ title: 'Null confidence candidate',
689
+ description: 'Should not be intakeable',
690
+ status: 'pending',
691
+ recommendationKind: 'principle',
692
+ confidence: null,
693
+ };
694
+ mockStateManager.getCandidate.mockResolvedValue(mockCandidate);
695
+
696
+ await handleCandidateIntake({
697
+ candidateId: 'null-conf-cand',
698
+ workspace: '/tmp/test-workspace',
699
+ json: true,
700
+ });
701
+
702
+ expect(mockService.intake).not.toHaveBeenCalled();
703
+ expect(consoleLogSpy).toHaveBeenCalledWith(
704
+ expect.stringContaining('confidence_missing_on_candidate_record')
705
+ );
706
+ expect(exitSpy).toHaveBeenCalledWith(1);
707
+ });
708
+
709
+ it('allows intake when candidate passes admission gate', async () => {
710
+ const mockCandidate = {
711
+ candidateId: 'admitted-cand',
712
+ artifactId: 'artifact-1',
713
+ taskId: 'task-1',
714
+ title: 'Admitted candidate',
715
+ description: 'Should be intakeable',
716
+ status: 'pending',
717
+ recommendationKind: 'principle',
718
+ confidence: 0.8,
719
+ };
720
+ mockStateManager.getCandidate.mockResolvedValue(mockCandidate);
721
+ mockService.intake.mockResolvedValue({ id: 'entry-1', title: 'Test', text: 'Test', status: 'probation' });
722
+
723
+ await handleCandidateIntake({
724
+ candidateId: 'admitted-cand',
725
+ workspace: '/tmp/test-workspace',
726
+ json: true,
727
+ });
728
+
729
+ expect(mockService.intake).toHaveBeenCalledWith('admitted-cand');
730
+ });
731
+ });
587
732
  });
@@ -477,6 +477,36 @@ describe('Commander wiring for backfill --include-pending', () => {
477
477
  expect(output.details.totalPending).toBe(1);
478
478
  });
479
479
 
480
+ // PRI-442 Stage 4: admission gate bypass prevention for backfill --include-pending
481
+ it('--include-pending refuses deferred candidate (admission gate bypass prevention)', async () => {
482
+ mockPrepareAll.mockImplementation((sql: string) => {
483
+ if (sql.includes("'consumed'")) return [];
484
+ if (sql.includes("'pending'")) return [{ candidate_id: 'cand-defer-1' }];
485
+ return [];
486
+ });
487
+ mockGetCandidate.mockResolvedValue({
488
+ candidateId: 'cand-defer-1',
489
+ taskId: 'diag-task-cand-defer-1',
490
+ description: 'deferred candidate',
491
+ recommendationKind: 'defer',
492
+ confidence: 0.9,
493
+ sourceRecommendationJson: JSON.stringify({ kind: 'defer', description: 'deferred' }),
494
+ });
495
+
496
+ const program = createBackfillTestProgram();
497
+ await program.parseAsync(['node', 'pd', 'candidate', 'internalization', 'backfill', '--workspace', WS, '--include-pending', '--confirm', '--json']);
498
+
499
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
500
+ // Should NOT have called intake for the deferred candidate
501
+ expect(mockIntake).not.toHaveBeenCalled();
502
+ // Should have an intake_failed result with admission gate reason
503
+ const failedResult = output.details.results.find(
504
+ (r: { status: string; reason: string }) => r.status === 'intake_failed' && r.reason.includes('Admission gate refused'),
505
+ );
506
+ expect(failedResult).toBeDefined();
507
+ expect(output.details.intakeFailed).toBe(1);
508
+ });
509
+
480
510
  it('--dry-run -> RuntimeStateManager readonly=true', async () => {
481
511
  const program = createBackfillTestProgram();
482
512
  await program.parseAsync(['node', 'pd', 'candidate', 'internalization', 'backfill', '--workspace', WS, '--dry-run', '--json']);
@@ -86,7 +86,6 @@ describe('PRI-455: pd runtime --help shows only MVP owner subcommands', () => {
86
86
  'canary',
87
87
  'synthetic',
88
88
  'uat',
89
- 'internalization',
90
89
  'recovery',
91
90
  'pruning',
92
91
  'diagnostics',
@@ -133,3 +132,18 @@ describe('PRI-455: hidden commands still function (de-surface, not delete)', ()
133
132
  expect(output).toContain('--apply');
134
133
  });
135
134
  });
135
+
136
+ describe('pd demo first-principle alias (Fix-24)', () => {
137
+ // cli-7-test-wiring: verify the alias resolves to the same command as story-a.
138
+ it('pd demo first-principle --help works and matches story-a', () => {
139
+ const aliasOutput = runPdHelp(['demo', 'first-principle', '--help']);
140
+ const storyAOutput = runPdHelp(['demo', 'story-a', '--help']);
141
+
142
+ // Both should show the same options
143
+ expect(aliasOutput).toContain('--workspace');
144
+ expect(aliasOutput).toContain('--json');
145
+ expect(aliasOutput).toContain('--channels');
146
+ // Alias output should be identical to story-a output
147
+ expect(aliasOutput).toBe(storyAOutput);
148
+ });
149
+ });
@@ -496,11 +496,22 @@ describe('CLI command wiring (pd console open)', () => {
496
496
  workspaceRoot = path.resolve(__dirname, '../../../..');
497
497
  cliPath = path.join(workspaceRoot, 'packages', 'pd-cli', 'dist', 'index.js');
498
498
  tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-console-open-test-'));
499
- // Fake a console install: create dir + dist/server.js with a minimal HTTP server
500
- const consoleDir = path.join(os.homedir(), '.openclaw', 'extensions', 'principles-disciple', 'console');
501
- fs.mkdirSync(path.join(consoleDir, 'dist'), { recursive: true });
499
+ // Isolated fake HOME so the tests never read from or rmSync-destroy — a
500
+ // real ~/.openclaw install on developer machines. runPd() points the CLI
501
+ // child at this home via __PD_CONSOLE_TEST_FAKE_HOME.
502
+ const fakeHome = path.join(tmp, 'home');
503
+ process.env.__PD_CONSOLE_TEST_FAKE_HOME = fakeHome;
504
+ // Fake a console install: dir + ESM package.json + dist/server.js with a
505
+ // minimal HTTP server. The package.json "type": "module" makes the fake
506
+ // server's module system deterministic regardless of whether a real
507
+ // extension-root package.json exists above the console dir.
508
+ const consoleDir = path.join(fakeHome, '.openclaw', 'extensions', 'principles-disciple', 'console');
509
+ fs.mkdirSync(path.join(consoleDir, 'dist', 'web'), { recursive: true });
510
+ fs.writeFileSync(path.join(consoleDir, 'package.json'), JSON.stringify({ name: 'fake-pd-console', version: '0.0.0', type: 'module' }, null, 2));
511
+ // EP-06 regression guard: dist/web/index.html must exist (PR #1169 fix)
512
+ fs.writeFileSync(path.join(consoleDir, 'dist', 'web', 'index.html'), '<!DOCTYPE html><html></html>');
502
513
  fs.writeFileSync(path.join(consoleDir, 'dist', 'server.js'), `
503
- const http = require('http');
514
+ import http from 'node:http';
504
515
  const args = process.argv.slice(2);
505
516
  const portIdx = args.indexOf('--port');
506
517
  const port = portIdx >= 0 ? parseInt(args[portIdx + 1]) : 3100;
@@ -522,11 +533,8 @@ describe('CLI command wiring (pd console open)', () => {
522
533
  });
523
534
 
524
535
  afterEach(() => {
536
+ delete process.env.__PD_CONSOLE_TEST_FAKE_HOME;
525
537
  try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
526
- try {
527
- const consoleDir = path.join(os.homedir(), '.openclaw', 'extensions', 'principles-disciple', 'console');
528
- fs.rmSync(consoleDir, { recursive: true, force: true });
529
- } catch { /* ignore */ }
530
538
  });
531
539
 
532
540
  it('console open subcommand is registered (pd console open --help)', () => {
@@ -569,6 +577,58 @@ describe('CLI command wiring (pd console open)', () => {
569
577
  expect(parsed).toHaveProperty('browserOpened');
570
578
  }, 10_000);
571
579
 
580
+ it('pd console open --json fresh spawn includes a positive integer serverPid (PRI-526)', async () => {
581
+ // Async stream-parse + guaranteed teardown. execFileSync patterns are
582
+ // unreliable here on Windows: the CLI keeps running after printing JSON
583
+ // (fresh spawn) or may unexpectedly take another branch, and killing via
584
+ // timeout can leave the grandchild server holding pipe handles.
585
+ let run: CliJsonRun | undefined;
586
+ try {
587
+ run = await runPdUntilJson(
588
+ ['console', 'open', '--workspace', tmp, '--port', '49390', '--json', '--no-browser'],
589
+ workspaceRoot,
590
+ );
591
+ if (!isRecord(run.parsed)) throw new Error('CLI JSON output was not an object');
592
+ expect(run.parsed.status).toBe('started');
593
+ expect(run.parsed.reused).toBe(false);
594
+ const serverPid = run.parsed.serverPid;
595
+ expect(typeof serverPid === 'number' && Number.isInteger(serverPid) && serverPid > 0).toBe(true);
596
+ } finally {
597
+ await teardownCliTree(run);
598
+ }
599
+ }, 20_000);
600
+
601
+ it('pd console open --json reused path does NOT include serverPid (PRI-526)', async () => {
602
+ // Stand up a fake healthy console in-process, then point the CLI at its
603
+ // port: planConsoleLaunch probes /api/health → 200 → reused, no spawn.
604
+ const server = http.createServer((req, res) => {
605
+ if (req.url === '/api/health') {
606
+ res.statusCode = 200;
607
+ res.end(JSON.stringify({ success: true }));
608
+ return;
609
+ }
610
+ res.statusCode = 404;
611
+ res.end();
612
+ });
613
+ await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', () => resolve()));
614
+ const addr = server.address();
615
+ if (typeof addr !== 'object' || !addr) throw new Error('no addr');
616
+ let run: CliJsonRun | undefined;
617
+ try {
618
+ run = await runPdUntilJson(
619
+ ['console', 'open', '--workspace', tmp, '--port', String(addr.port), '--json', '--no-browser'],
620
+ workspaceRoot,
621
+ );
622
+ if (!isRecord(run.parsed)) throw new Error('CLI JSON output was not an object');
623
+ expect(run.parsed.status).toBe('reused');
624
+ expect(run.parsed.reused).toBe(true);
625
+ expect(run.parsed).not.toHaveProperty('serverPid');
626
+ } finally {
627
+ await teardownCliTree(run);
628
+ server.close();
629
+ }
630
+ }, 20_000);
631
+
572
632
  it('pd console open --port 99999 --json returns a structured failure (invalid port)', () => {
573
633
  const out = runPd(['console', 'open', '--workspace', tmp, '--port', '99999', '--json', '--no-browser'], workspaceRoot);
574
634
  const parsed = JSON.parse(out);
@@ -666,11 +726,22 @@ describe('CLI command wiring (pd console open)', () => {
666
726
 
667
727
  it('sets browserOpened: false when browser fails to open', async () => {
668
728
  const { handleConsoleOpen } = await import('../../src/commands/console.js');
669
-
729
+
670
730
  const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
671
731
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
672
732
  const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
673
733
 
734
+ // The in-process handler resolves the console dir via getConsoleDir(),
735
+ // which reads HOME/USERPROFILE directly. Point them at the isolated
736
+ // fake home — a clean CI runner has no real ~/.openclaw console, and
737
+ // relying on one (as this test implicitly did before) makes the result
738
+ // machine-dependent.
739
+ const fakeHome = process.env.__PD_CONSOLE_TEST_FAKE_HOME ?? '';
740
+ const savedHome = process.env.HOME;
741
+ const savedUserProfile = process.env.USERPROFILE;
742
+ process.env.HOME = fakeHome;
743
+ process.env.USERPROFILE = fakeHome;
744
+
674
745
  const mockChild = new EventEmitter() as any;
675
746
  mockChild.unref = vi.fn();
676
747
  let spawnCalled = false;
@@ -698,23 +769,29 @@ describe('CLI command wiring (pd console open)', () => {
698
769
  };
699
770
  };
700
771
 
701
- await handleConsoleOpen({
702
- workspace: tmp,
703
- json: false,
704
- });
772
+ try {
773
+ await handleConsoleOpen({
774
+ workspace: tmp,
775
+ json: false,
776
+ });
705
777
 
706
- await new Promise(resolve => setTimeout(resolve, 150));
778
+ await new Promise(resolve => setTimeout(resolve, 150));
707
779
 
708
- const loggedOutput = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
709
- expect(exitSpy).not.toHaveBeenCalled();
710
- expect(spawnCalled).toBe(true);
711
-
712
- expect(loggedOutput).not.toContain('Browser opened');
713
- expect(loggedOutput).toContain('Open http://127.0.0.1:3100 in your browser');
780
+ const loggedOutput = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
781
+ expect(exitSpy).not.toHaveBeenCalled();
782
+ expect(spawnCalled).toBe(true);
714
783
 
715
- exitSpy.mockRestore();
716
- logSpy.mockRestore();
717
- errorSpy.mockRestore();
784
+ expect(loggedOutput).not.toContain('Browser opened');
785
+ expect(loggedOutput).toContain('Open http://127.0.0.1:3100 in your browser');
786
+ } finally {
787
+ if (savedHome === undefined) delete process.env.HOME;
788
+ else process.env.HOME = savedHome;
789
+ if (savedUserProfile === undefined) delete process.env.USERPROFILE;
790
+ else process.env.USERPROFILE = savedUserProfile;
791
+ exitSpy.mockRestore();
792
+ logSpy.mockRestore();
793
+ errorSpy.mockRestore();
794
+ }
718
795
  });
719
796
  });
720
797
 
@@ -752,6 +829,13 @@ describe('CLI command wiring (pd console open)', () => {
752
829
  it('[::1] is accepted and normalized to ::1 (not refused)', () => {
753
830
  // May spawn a long-lived server, use timeout.
754
831
  const out = runPd(['console', 'open', '--workspace', tmp, '--host', '[::1]', '--json', '--no-browser'], workspaceRoot, 8_000);
832
+ if (out.trim() === '') {
833
+ // The CLI was still in its 15s ready-poll when execFileSync timed out.
834
+ // Known environment limitation: on Windows hosts where IPv6 loopback
835
+ // connections are refused (EACCES), the health probe on ::1 never
836
+ // succeeds. CI (Linux) is unaffected.
837
+ throw new Error('CLI produced no JSON within 8s — ::1 health probe never succeeded (IPv6 loopback may be blocked on this machine)');
838
+ }
755
839
  const parsed = JSON.parse(out);
756
840
  // Should NOT be refused — [::1] is loopback
757
841
  expect(parsed.status).not.toBe('refused');
@@ -759,22 +843,30 @@ describe('CLI command wiring (pd console open)', () => {
759
843
  expect(parsed.host).toBe('::1');
760
844
  }, 10_000);
761
845
  });
846
+
847
+ describe('EP-06 regression guard: dist/web/index.html check (PR #1169)', () => {
848
+ it('returns console_web_ui_missing when dist/web/index.html does not exist', () => {
849
+ // beforeEach creates the fake console dir WITH dist/web/index.html.
850
+ // Remove it to simulate a corrupted install (the PR #1169 regression).
851
+ const fakeHome = process.env.__PD_CONSOLE_TEST_FAKE_HOME ?? '';
852
+ const webIndex = path.join(fakeHome, '.openclaw', 'extensions', 'principles-disciple', 'console', 'dist', 'web', 'index.html');
853
+ fs.rmSync(webIndex, { force: true });
854
+
855
+ const out = runPd(['console', 'open', '--workspace', tmp, '--json', '--no-browser'], workspaceRoot, 5_000);
856
+ const parsed = JSON.parse(out);
857
+ expect(parsed.status).toBe('failed');
858
+ expect(parsed.reason).toBe('console_web_ui_missing');
859
+ expect(parsed.nextAction).toMatch(/create-principles-disciple/);
860
+ }, 10_000);
861
+ });
762
862
  });
763
863
 
764
864
  function runPd(args: string[], cwd: string, timeoutMs?: number): string {
765
865
  try {
766
- const env: Record<string, string> = { ...process.env };
767
- if (!args.includes('--workspace') && !args.includes('--help') && !args.includes('-h')) {
768
- env.USERPROFILE = '/nonexistent';
769
- env.HOME = '/nonexistent';
770
- env.HOMEPATH = '/nonexistent';
771
- env.HOMEDRIVE = '/nonexistent';
772
- delete env.PD_WORKSPACE_DIR;
773
- }
774
866
  return execFileSync('node', [getBuiltPdCliPath(), ...args], {
775
867
  encoding: 'utf8',
776
868
  cwd,
777
- env,
869
+ env: buildChildEnv(),
778
870
  timeout: timeoutMs,
779
871
  });
780
872
  } catch (err: unknown) {
@@ -787,3 +879,108 @@ function runPd(args: string[], cwd: string, timeoutMs?: number): string {
787
879
  throw err;
788
880
  }
789
881
  }
882
+
883
+ /** Env for spawned CLI processes: isolated fake HOME, no workspace env leaks. */
884
+ function buildChildEnv(): Record<string, string> {
885
+ const env = { ...process.env } as Record<string, string>;
886
+ const fakeHome = process.env.__PD_CONSOLE_TEST_FAKE_HOME;
887
+ if (fakeHome) {
888
+ env.USERPROFILE = fakeHome;
889
+ env.HOME = fakeHome;
890
+ delete env.HOMEPATH;
891
+ delete env.HOMEDRIVE;
892
+ delete env.PD_WORKSPACE_DIR;
893
+ }
894
+ return env;
895
+ }
896
+
897
+ /** Runtime type guard for unknown CLI JSON output (rc-1/rc-2). */
898
+ function isRecord(value: unknown): value is Record<string, unknown> {
899
+ return typeof value === 'object' && value !== null;
900
+ }
901
+
902
+ // ─── Async CLI runner with guaranteed teardown (PRI-526 tests) ───────────────
903
+
904
+ interface CliJsonRun {
905
+ parsed: unknown;
906
+ child: childProcessModule.ChildProcess;
907
+ }
908
+
909
+ /**
910
+ * Spawn the CLI and resolve as soon as stdout contains one parseable JSON
911
+ * object. Fails fast (bounded timeout / early exit) with the collected
912
+ * stdout in the error message. The caller MUST call teardownCliTree(run)
913
+ * in a finally block — the CLI process may still be alive after the JSON
914
+ * is emitted (fresh-spawn mode keeps the console server attached).
915
+ */
916
+ async function runPdUntilJson(args: string[], cwd: string, timeoutMs = 12_000): Promise<CliJsonRun> {
917
+ const child = childProcessModule.spawn('node', [getBuiltPdCliPath(), ...args], {
918
+ cwd,
919
+ env: buildChildEnv(),
920
+ });
921
+ let stdout = '';
922
+ child.stdout?.on('data', (d: Buffer) => { stdout += d.toString('utf8'); });
923
+
924
+ const parsed: unknown = await new Promise<unknown>((resolve, reject) => {
925
+ let done = false;
926
+ const fail = (msg: string): void => {
927
+ if (done) return;
928
+ done = true;
929
+ reject(new Error(`${msg}; stdout so far: ${stdout.slice(0, 400)}`));
930
+ };
931
+ const timer = setTimeout(() => fail('timed out waiting for CLI JSON'), timeoutMs);
932
+ const tryParse = (): boolean => {
933
+ const trimmed = stdout.trim();
934
+ if (!trimmed.startsWith('{')) return false;
935
+ try {
936
+ const value = JSON.parse(trimmed) as unknown;
937
+ done = true;
938
+ clearTimeout(timer);
939
+ resolve(value);
940
+ return true;
941
+ } catch {
942
+ return false; // partial pretty-printed JSON — wait for more chunks
943
+ }
944
+ };
945
+ if (tryParse()) return;
946
+ const onData = (): void => {
947
+ if (tryParse()) child.stdout?.off('data', onData);
948
+ };
949
+ child.stdout?.on('data', onData);
950
+ child.on('error', (err: Error) => fail(`CLI spawn error: ${err.message}`));
951
+ child.on('exit', (code: number | null) => fail(`CLI exited with code ${code} before emitting JSON`));
952
+ });
953
+
954
+ return { parsed, child };
955
+ }
956
+
957
+ /**
958
+ * Deterministic teardown of a runPdUntilJson run: kill the reported console
959
+ * server PID (grandchild) first so the CLI can exit on its own, then wait a
960
+ * bounded time, then force-kill the whole CLI tree. Safe to call multiple
961
+ * times and with undefined.
962
+ */
963
+ async function teardownCliTree(run: CliJsonRun | undefined): Promise<void> {
964
+ if (!run) return;
965
+ const record = isRecord(run.parsed) ? run.parsed : undefined;
966
+ const serverPid = record !== undefined && typeof record.serverPid === 'number' ? record.serverPid : undefined;
967
+ if (serverPid !== undefined) {
968
+ try { process.kill(serverPid); } catch { /* already gone */ }
969
+ }
970
+ await new Promise<void>((resolve) => {
971
+ const t = setTimeout(() => { killTreeForce(run.child); resolve(); }, 4_000);
972
+ run.child.once('exit', () => { clearTimeout(t); resolve(); });
973
+ });
974
+ killTreeForce(run.child);
975
+ }
976
+
977
+ /**
978
+ * Last-resort kill of the CLI child process. The console server grandchild is
979
+ * reaped separately via its reported serverPid (see teardownCliTree), so a
980
+ * plain signal kill suffices — no shell/argv command wrapper needed.
981
+ */
982
+ function killTreeForce(child: childProcessModule.ChildProcess): void {
983
+ try {
984
+ child.kill('SIGKILL');
985
+ } catch { /* already gone */ }
986
+ }