@principles/pd-cli 1.133.0 → 1.134.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 (211) 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 +40 -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/pain-flood-simulation-runner.d.ts.map +1 -1
  107. package/dist/services/pain-flood-simulation-runner.js +9 -0
  108. package/dist/services/pain-flood-simulation-runner.js.map +1 -1
  109. package/dist/services/pd-config-loader.js +2 -2
  110. package/dist/services/pd-config-loader.js.map +1 -1
  111. package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
  112. package/dist/services/proven-channel-baseline-runner.js +10 -2
  113. package/dist/services/proven-channel-baseline-runner.js.map +1 -1
  114. package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
  115. package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
  116. package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
  117. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  118. package/dist/services/rulehost-pipeline-runner.js +111 -7
  119. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  120. package/dist/services/rulehost-readiness.js.map +1 -1
  121. package/dist/services/runtime-adapter-resolver.d.ts +1 -0
  122. package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
  123. package/dist/services/runtime-adapter-resolver.js +3 -0
  124. package/dist/services/runtime-adapter-resolver.js.map +1 -1
  125. package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
  126. package/dist/services/synthetic-baseline-runner.js +11 -0
  127. package/dist/services/synthetic-baseline-runner.js.map +1 -1
  128. package/dist/utils/production-workspace-guard.d.ts +4 -4
  129. package/dist/utils/production-workspace-guard.d.ts.map +1 -1
  130. package/dist/utils/production-workspace-guard.js +19 -16
  131. package/dist/utils/production-workspace-guard.js.map +1 -1
  132. package/dist/utils/production-workspace-guard.test.js +54 -19
  133. package/dist/utils/production-workspace-guard.test.js.map +1 -1
  134. package/package.json +8 -5
  135. package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
  136. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
  137. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
  138. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
  139. package/src/commands/admission-gate.ts +30 -0
  140. package/src/commands/candidate.ts +74 -6
  141. package/src/commands/console.ts +40 -0
  142. package/src/commands/demo-story-a.ts +4 -2
  143. package/src/commands/diagnose.ts +158 -10
  144. package/src/commands/errors-list.ts +440 -0
  145. package/src/commands/health-codex.ts +249 -0
  146. package/src/commands/health.ts +60 -0
  147. package/src/commands/intent.ts +98 -29
  148. package/src/commands/pain-evidence.ts +6 -0
  149. package/src/commands/pain-record.ts +21 -0
  150. package/src/commands/pain-retry.ts +353 -35
  151. package/src/commands/proven-channel-baseline.ts +4 -2
  152. package/src/commands/runtime-activation.ts +657 -12
  153. package/src/commands/runtime-canary.ts +31 -5
  154. package/src/commands/runtime-health-snapshot.ts +4 -2
  155. package/src/commands/runtime-init.ts +305 -0
  156. package/src/commands/runtime-internalization-context-trace.ts +445 -0
  157. package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
  158. package/src/commands/runtime-internalization-integrity.ts +4 -2
  159. package/src/commands/runtime-internalization-run-once.ts +20 -5
  160. package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
  161. package/src/commands/runtime-uat.guard.test.ts +14 -16
  162. package/src/commands/runtime-uat.ts +4 -3
  163. package/src/config-reader.ts +1 -1
  164. package/src/index.ts +69 -78
  165. package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
  166. package/src/services/config-doctor.ts +9 -2
  167. package/src/services/pain-flood-simulation-runner.ts +9 -0
  168. package/src/services/pd-config-loader.ts +2 -2
  169. package/src/services/proven-channel-baseline-runner.ts +10 -3
  170. package/src/services/rulehost-pipeline-runner.ts +157 -6
  171. package/src/services/runtime-adapter-resolver.ts +4 -0
  172. package/src/services/synthetic-baseline-runner.ts +11 -0
  173. package/src/utils/production-workspace-guard.test.ts +62 -20
  174. package/src/utils/production-workspace-guard.ts +19 -16
  175. package/tests/bdd/cli-contract.steps.ts +343 -0
  176. package/tests/bdd/context-trace.steps.ts +229 -0
  177. package/tests/bdd/support/gherkin-loader.ts +147 -0
  178. package/tests/bdd/support/repo-root.ts +62 -0
  179. package/tests/bdd/support/vitest-bdd.ts +179 -0
  180. package/tests/commands/admission-gate-coverage.test.ts +79 -0
  181. package/tests/commands/candidate-audit-repair.test.ts +77 -0
  182. package/tests/commands/candidate-intake.test.ts +155 -10
  183. package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
  184. package/tests/commands/cli-help-snapshot.test.ts +15 -1
  185. package/tests/commands/console-open.test.ts +19 -0
  186. package/tests/commands/diagnose.test.ts +619 -14
  187. package/tests/commands/errors-list.test.ts +678 -0
  188. package/tests/commands/health-codex.test.ts +243 -0
  189. package/tests/commands/health-host-wiring.test.ts +76 -0
  190. package/tests/commands/intent.test.ts +4 -4
  191. package/tests/commands/pain-record.test.ts +80 -0
  192. package/tests/commands/pain-retry.test.ts +94 -3
  193. package/tests/commands/run-rulehost-handler.test.ts +133 -0
  194. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
  195. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
  196. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
  197. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
  198. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
  199. package/tests/commands/runtime-activation.test.ts +1090 -5
  200. package/tests/commands/runtime-canary.test.ts +50 -0
  201. package/tests/commands/runtime-features.test.ts +2 -0
  202. package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
  203. package/tests/commands/runtime-init.test.ts +346 -0
  204. package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
  205. package/tests/commands/runtime-internalization.test.ts +2 -2
  206. package/tests/config-reader.test.ts +6 -5
  207. package/tests/e2e/cross-package-acceptance.test.ts +55 -13
  208. package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
  209. package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
  210. package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
  211. package/vitest.config.ts +1 -1
@@ -96,6 +96,18 @@ vi.mock('@principles/core/runtime-v2', () => ({
96
96
  }),
97
97
  resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
98
98
  PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
99
+ evaluateCandidateAdmissionFromRecord: vi.fn((candidate: { recommendationKind: string; confidence: number | null }) => {
100
+ if (candidate.recommendationKind === 'defer') {
101
+ return { decision: 'deferred', reason: 'recommendation_kind_defer_not_actionable', nextAction: 'review_defer_disposition_manually', evidenceStatus: 'unknown' };
102
+ }
103
+ if (candidate.confidence === null) {
104
+ return { decision: 'needs_evidence', reason: 'confidence_missing_on_candidate_record', nextAction: 're_run_diagnosis_to_populate_confidence_or_manual_review', evidenceStatus: 'unknown' };
105
+ }
106
+ if (candidate.confidence !== undefined && candidate.confidence < 0.5) {
107
+ return { decision: 'needs_evidence', reason: `confidence_below_threshold:${candidate.confidence.toFixed(2)}<0.5`, nextAction: 'provide_additional_evidence_or_manual_review', evidenceStatus: 'unknown' };
108
+ }
109
+ return { decision: 'admitted', reason: 'cli_partial_check_passed_confidence_and_kind', nextAction: 'none', evidenceStatus: 'unknown' };
110
+ }),
99
111
  }));
100
112
 
101
113
  // PRI-443 Phase 5: loadLedger and getLedgerFilePathPublic now imported from
@@ -341,4 +353,69 @@ describe('pd candidate repair', () => {
341
353
  );
342
354
  expect(exitSpy).toHaveBeenCalledWith(1);
343
355
  });
356
+
357
+ // PRI-442 Stage 4: admission gate bypass prevention for repair
358
+ describe('admission gate bypass prevention (PRI-442 Stage 4)', () => {
359
+ it('refuses repair when recommendationKind is defer', async () => {
360
+ mockStateManager.getCandidate.mockResolvedValue({
361
+ candidateId: 'c-defer',
362
+ status: 'consumed',
363
+ artifactId: 'a1',
364
+ title: 'Deferred',
365
+ description: 'desc',
366
+ recommendationKind: 'defer',
367
+ confidence: 0.9,
368
+ });
369
+
370
+ await handleCandidateRepair({ candidateId: 'c-defer', workspace: '/tmp/test-workspace', json: true });
371
+
372
+ expect(consoleLogSpy).toHaveBeenCalledWith(
373
+ expect.stringContaining('"status": "refused"'),
374
+ );
375
+ expect(consoleLogSpy).toHaveBeenCalledWith(
376
+ expect.stringContaining('"admissionDecision": "deferred"'),
377
+ );
378
+ expect(exitSpy).toHaveBeenCalledWith(1);
379
+ });
380
+
381
+ it('refuses repair when confidence is null (rc-3 fail loud)', async () => {
382
+ mockStateManager.getCandidate.mockResolvedValue({
383
+ candidateId: 'c-null',
384
+ status: 'consumed',
385
+ artifactId: 'a1',
386
+ title: 'Null conf',
387
+ description: 'desc',
388
+ recommendationKind: 'principle',
389
+ confidence: null,
390
+ });
391
+
392
+ await handleCandidateRepair({ candidateId: 'c-null', workspace: '/tmp/test-workspace', json: false });
393
+
394
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
395
+ expect.stringContaining('Admission gate refused'),
396
+ );
397
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
398
+ expect.stringContaining('needs_evidence'),
399
+ );
400
+ expect(exitSpy).toHaveBeenCalledWith(1);
401
+ });
402
+
403
+ it('allows repair when candidate passes admission gate', async () => {
404
+ mockStateManager.getCandidate.mockResolvedValue({
405
+ candidateId: 'c-ok',
406
+ status: 'consumed',
407
+ artifactId: 'a1',
408
+ title: 'OK',
409
+ description: 'desc',
410
+ recommendationKind: 'principle',
411
+ confidence: 0.8,
412
+ });
413
+ mockAdapter.existsForCandidate.mockReturnValue(null);
414
+ mockService.intake.mockResolvedValue({ id: 'entry-1', title: 'OK', text: 'text', status: 'probation' });
415
+
416
+ await handleCandidateRepair({ candidateId: 'c-ok', workspace: '/tmp/test-workspace', json: true });
417
+
418
+ expect(mockService.intake).toHaveBeenCalledWith('c-ok');
419
+ });
420
+ });
344
421
  });
@@ -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
+ });
@@ -499,6 +499,9 @@ describe('CLI command wiring (pd console open)', () => {
499
499
  // Fake a console install: create dir + dist/server.js with a minimal HTTP server
500
500
  const consoleDir = path.join(os.homedir(), '.openclaw', 'extensions', 'principles-disciple', 'console');
501
501
  fs.mkdirSync(path.join(consoleDir, 'dist'), { recursive: true });
502
+ // EP-06 regression guard: dist/web/index.html must exist (PR #1169 fix)
503
+ fs.mkdirSync(path.join(consoleDir, 'dist', 'web'), { recursive: true });
504
+ fs.writeFileSync(path.join(consoleDir, 'dist', 'web', 'index.html'), '<!DOCTYPE html><html></html>');
502
505
  fs.writeFileSync(path.join(consoleDir, 'dist', 'server.js'), `
503
506
  const http = require('http');
504
507
  const args = process.argv.slice(2);
@@ -759,6 +762,22 @@ describe('CLI command wiring (pd console open)', () => {
759
762
  expect(parsed.host).toBe('::1');
760
763
  }, 10_000);
761
764
  });
765
+
766
+ describe('EP-06 regression guard: dist/web/index.html check (PR #1169)', () => {
767
+ it('returns console_web_ui_missing when dist/web/index.html does not exist', () => {
768
+ // beforeEach creates the fake console dir WITH dist/web/index.html.
769
+ // Remove it to simulate a corrupted install (the PR #1169 regression).
770
+ const consoleDir = path.join(os.homedir(), '.openclaw', 'extensions', 'principles-disciple', 'console');
771
+ const webIndex = path.join(consoleDir, 'dist', 'web', 'index.html');
772
+ fs.rmSync(webIndex, { force: true });
773
+
774
+ const out = runPd(['console', 'open', '--workspace', tmp, '--json', '--no-browser'], workspaceRoot, 5_000);
775
+ const parsed = JSON.parse(out);
776
+ expect(parsed.status).toBe('failed');
777
+ expect(parsed.reason).toBe('console_web_ui_missing');
778
+ expect(parsed.nextAction).toMatch(/create-principles-disciple/);
779
+ }, 10_000);
780
+ });
762
781
  });
763
782
 
764
783
  function runPd(args: string[], cwd: string, timeoutMs?: number): string {