create-principles-disciple 1.144.11 → 1.144.13

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 (157) hide show
  1. package/_release/product-identity.json +1 -1
  2. package/codex-adapter/dist/pd-hook.js +23 -5
  3. package/codex-adapter/dist/worker/workspace-worker.d.ts +6 -0
  4. package/codex-adapter/dist/worker/workspace-worker.js +7 -0
  5. package/codex-adapter/package.json +1 -1
  6. package/console/dist/server/models/ApprovalsConsoleModel.d.ts +1 -1
  7. package/console/dist/server/models/ApprovalsConsoleModel.js +4 -4
  8. package/console/dist/server/models/ApprovalsGroupedConsoleModel.d.ts +5 -5
  9. package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +18 -18
  10. package/console/dist/shared/prompt-injection-contract.d.ts +5 -5
  11. package/console/dist/ui/pages/focus/FocusPage.js +1 -1
  12. package/console/dist/ui/pages/focus/focus-validation.js +1 -1
  13. package/console/dist/ui/utils/approval-warning-localization.js +1 -1
  14. package/console/dist/ui/utils/validators.d.ts +1 -1
  15. package/console/dist/ui/utils/validators.js +2 -2
  16. package/console/package.json +3 -3
  17. package/core/dist/principle-tree-ledger.d.ts +50 -0
  18. package/core/dist/principle-tree-ledger.d.ts.map +1 -1
  19. package/core/dist/principle-tree-ledger.js +133 -1
  20. package/core/dist/principle-tree-ledger.js.map +1 -1
  21. package/core/dist/runtime-v2/adapter/output-schema-registry.d.ts.map +1 -1
  22. package/core/dist/runtime-v2/adapter/output-schema-registry.js +4 -0
  23. package/core/dist/runtime-v2/adapter/output-schema-registry.js.map +1 -1
  24. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.d.ts +24 -0
  25. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.d.ts.map +1 -1
  26. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.js +36 -1
  27. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.js.map +1 -1
  28. package/core/dist/runtime-v2/bridge-result-shaper.d.ts +8 -0
  29. package/core/dist/runtime-v2/bridge-result-shaper.d.ts.map +1 -1
  30. package/core/dist/runtime-v2/bridge-result-shaper.js +61 -3
  31. package/core/dist/runtime-v2/bridge-result-shaper.js.map +1 -1
  32. package/core/dist/runtime-v2/candidate-intake-service.d.ts +152 -3
  33. package/core/dist/runtime-v2/candidate-intake-service.d.ts.map +1 -1
  34. package/core/dist/runtime-v2/candidate-intake-service.js +405 -47
  35. package/core/dist/runtime-v2/candidate-intake-service.js.map +1 -1
  36. package/core/dist/runtime-v2/candidate-intake.d.ts +44 -0
  37. package/core/dist/runtime-v2/candidate-intake.d.ts.map +1 -1
  38. package/core/dist/runtime-v2/candidate-intake.js +7 -0
  39. package/core/dist/runtime-v2/candidate-intake.js.map +1 -1
  40. package/core/dist/runtime-v2/config/index.d.ts +2 -2
  41. package/core/dist/runtime-v2/config/index.d.ts.map +1 -1
  42. package/core/dist/runtime-v2/config/index.js +1 -1
  43. package/core/dist/runtime-v2/config/index.js.map +1 -1
  44. package/core/dist/runtime-v2/config/pd-config-defaults.d.ts +7 -1
  45. package/core/dist/runtime-v2/config/pd-config-defaults.d.ts.map +1 -1
  46. package/core/dist/runtime-v2/config/pd-config-defaults.js +11 -0
  47. package/core/dist/runtime-v2/config/pd-config-defaults.js.map +1 -1
  48. package/core/dist/runtime-v2/config/pd-config-effective.d.ts.map +1 -1
  49. package/core/dist/runtime-v2/config/pd-config-effective.js +24 -1
  50. package/core/dist/runtime-v2/config/pd-config-effective.js.map +1 -1
  51. package/core/dist/runtime-v2/config/pd-config-redaction.d.ts.map +1 -1
  52. package/core/dist/runtime-v2/config/pd-config-redaction.js +2 -0
  53. package/core/dist/runtime-v2/config/pd-config-redaction.js.map +1 -1
  54. package/core/dist/runtime-v2/config/pd-config-types.d.ts +39 -0
  55. package/core/dist/runtime-v2/config/pd-config-types.d.ts.map +1 -1
  56. package/core/dist/runtime-v2/config/pd-config-types.js.map +1 -1
  57. package/core/dist/runtime-v2/config/pd-config-validate.d.ts.map +1 -1
  58. package/core/dist/runtime-v2/config/pd-config-validate.js +44 -0
  59. package/core/dist/runtime-v2/config/pd-config-validate.js.map +1 -1
  60. package/core/dist/runtime-v2/index.d.ts +14 -1
  61. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  62. package/core/dist/runtime-v2/index.js +21 -1
  63. package/core/dist/runtime-v2/index.js.map +1 -1
  64. package/core/dist/runtime-v2/pain-signal-bridge.d.ts.map +1 -1
  65. package/core/dist/runtime-v2/pain-signal-bridge.js +107 -2
  66. package/core/dist/runtime-v2/pain-signal-bridge.js.map +1 -1
  67. package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts +29 -14
  68. package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
  69. package/core/dist/runtime-v2/pain-signal-runtime-factory.js +215 -46
  70. package/core/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
  71. package/core/dist/runtime-v2/principle-reuse/reuse-domain.d.ts +89 -0
  72. package/core/dist/runtime-v2/principle-reuse/reuse-domain.d.ts.map +1 -0
  73. package/core/dist/runtime-v2/principle-reuse/reuse-domain.js +16 -0
  74. package/core/dist/runtime-v2/principle-reuse/reuse-domain.js.map +1 -0
  75. package/core/dist/runtime-v2/principle-reuse/reuse-evaluation-output.d.ts +40 -0
  76. package/core/dist/runtime-v2/principle-reuse/reuse-evaluation-output.d.ts.map +1 -0
  77. package/core/dist/runtime-v2/principle-reuse/reuse-evaluation-output.js +97 -0
  78. package/core/dist/runtime-v2/principle-reuse/reuse-evaluation-output.js.map +1 -0
  79. package/core/dist/runtime-v2/principle-reuse/reuse-evaluation-runner.d.ts +111 -0
  80. package/core/dist/runtime-v2/principle-reuse/reuse-evaluation-runner.d.ts.map +1 -0
  81. package/core/dist/runtime-v2/principle-reuse/reuse-evaluation-runner.js +232 -0
  82. package/core/dist/runtime-v2/principle-reuse/reuse-evaluation-runner.js.map +1 -0
  83. package/core/dist/runtime-v2/principle-reuse/reuse-proposal.d.ts +87 -0
  84. package/core/dist/runtime-v2/principle-reuse/reuse-proposal.d.ts.map +1 -0
  85. package/core/dist/runtime-v2/principle-reuse/reuse-proposal.js +99 -0
  86. package/core/dist/runtime-v2/principle-reuse/reuse-proposal.js.map +1 -0
  87. package/core/dist/runtime-v2/principle-reuse/reuse-retrieval.d.ts +36 -0
  88. package/core/dist/runtime-v2/principle-reuse/reuse-retrieval.d.ts.map +1 -0
  89. package/core/dist/runtime-v2/principle-reuse/reuse-retrieval.js +118 -0
  90. package/core/dist/runtime-v2/principle-reuse/reuse-retrieval.js.map +1 -0
  91. package/core/dist/runtime-v2/principle-reuse/reuse-shortlist.d.ts +40 -0
  92. package/core/dist/runtime-v2/principle-reuse/reuse-shortlist.d.ts.map +1 -0
  93. package/core/dist/runtime-v2/principle-reuse/reuse-shortlist.js +184 -0
  94. package/core/dist/runtime-v2/principle-reuse/reuse-shortlist.js.map +1 -0
  95. package/core/dist/runtime-v2/store/pain-diagnosis/pain-diagnosis-store.d.ts +7 -0
  96. package/core/dist/runtime-v2/store/pain-diagnosis/pain-diagnosis-store.d.ts.map +1 -1
  97. package/core/dist/runtime-v2/store/pain-diagnosis/pain-diagnosis-store.js.map +1 -1
  98. package/core/dist/runtime-v2/store/pain-diagnosis/sqlite-pain-diagnosis-store.d.ts +1 -0
  99. package/core/dist/runtime-v2/store/pain-diagnosis/sqlite-pain-diagnosis-store.d.ts.map +1 -1
  100. package/core/dist/runtime-v2/store/pain-diagnosis/sqlite-pain-diagnosis-store.js +12 -0
  101. package/core/dist/runtime-v2/store/pain-diagnosis/sqlite-pain-diagnosis-store.js.map +1 -1
  102. package/core/dist/runtime-v2/store/runtime-state-manager.d.ts +5 -0
  103. package/core/dist/runtime-v2/store/runtime-state-manager.d.ts.map +1 -1
  104. package/core/dist/runtime-v2/store/runtime-state-manager.js +8 -0
  105. package/core/dist/runtime-v2/store/runtime-state-manager.js.map +1 -1
  106. package/core/dist/runtime-v2/types/principle-schema.d.ts +65 -0
  107. package/core/dist/runtime-v2/types/principle-schema.d.ts.map +1 -1
  108. package/core/dist/runtime-v2/types/principle-schema.js +14 -0
  109. package/core/dist/runtime-v2/types/principle-schema.js.map +1 -1
  110. package/core/dist/runtime-v2/types/principle-tree-store.d.ts +12 -0
  111. package/core/dist/runtime-v2/types/principle-tree-store.d.ts.map +1 -1
  112. package/core/dist/telemetry-event.d.ts +2 -2
  113. package/core/dist/telemetry-event.d.ts.map +1 -1
  114. package/core/dist/telemetry-event.js +13 -0
  115. package/core/dist/telemetry-event.js.map +1 -1
  116. package/core/package.json +2 -2
  117. package/host-runtime/dist/index.d.ts +2 -0
  118. package/host-runtime/dist/index.js +1 -0
  119. package/host-runtime/dist/production-rulehost-gate.d.ts +1 -0
  120. package/host-runtime/dist/production-rulehost-gate.js +1 -1
  121. package/host-runtime/dist/prompt-injection-projection.d.ts +5 -5
  122. package/host-runtime/dist/prompt-injection-projection.js +4 -4
  123. package/host-runtime/package.json +1 -1
  124. package/package.json +3 -3
  125. package/pd-cli/dist/commands/candidate.d.ts +27 -0
  126. package/pd-cli/dist/commands/candidate.d.ts.map +1 -1
  127. package/pd-cli/dist/commands/candidate.js +508 -14
  128. package/pd-cli/dist/commands/candidate.js.map +1 -1
  129. package/pd-cli/dist/commands/config-doctor.d.ts.map +1 -1
  130. package/pd-cli/dist/commands/config-doctor.js +14 -0
  131. package/pd-cli/dist/commands/config-doctor.js.map +1 -1
  132. package/pd-cli/dist/commands/diagnose.d.ts.map +1 -1
  133. package/pd-cli/dist/commands/diagnose.js +84 -14
  134. package/pd-cli/dist/commands/diagnose.js.map +1 -1
  135. package/pd-cli/dist/commands/pain-retry.d.ts.map +1 -1
  136. package/pd-cli/dist/commands/pain-retry.js +97 -14
  137. package/pd-cli/dist/commands/pain-retry.js.map +1 -1
  138. package/pd-cli/dist/index.js +16 -2
  139. package/pd-cli/dist/index.js.map +1 -1
  140. package/pd-cli/dist/services/config-doctor.d.ts +11 -0
  141. package/pd-cli/dist/services/config-doctor.d.ts.map +1 -1
  142. package/pd-cli/dist/services/config-doctor.js +6 -0
  143. package/pd-cli/dist/services/config-doctor.js.map +1 -1
  144. package/pd-cli/dist/services/resolve-runtime-from-pd-config.d.ts +14 -0
  145. package/pd-cli/dist/services/resolve-runtime-from-pd-config.d.ts.map +1 -1
  146. package/pd-cli/dist/services/resolve-runtime-from-pd-config.js +20 -4
  147. package/pd-cli/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
  148. package/pd-cli/dist/services/runtime-adapter-resolver.d.ts +6 -0
  149. package/pd-cli/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
  150. package/pd-cli/dist/services/runtime-adapter-resolver.js +11 -2
  151. package/pd-cli/dist/services/runtime-adapter-resolver.js.map +1 -1
  152. package/pd-cli/package.json +1 -1
  153. package/plugin/dist/bundle.js +550 -546
  154. package/plugin/dist/openclaw.plugin.json +1 -1
  155. package/plugin/openclaw.plugin.json +1 -1
  156. package/plugin/package.json +5 -5
  157. package/release-manager/package.json +3 -3
@@ -1,19 +1,27 @@
1
1
  /**
2
- * pd candidate commands — Principle candidate inspection, intake, audit, repair.
2
+ * pd candidate commands — Principle candidate inspection, intake, audit, repair, reuse review.
3
3
  *
4
4
  * Usage:
5
5
  * pd candidate list --task-id <taskId> --workspace <path> [--json]
6
6
  * pd candidate show <candidateId> --workspace <path> [--json]
7
7
  * pd candidate intake --candidate-id <id> [--workspace <path>] [--json] [--dry-run]
8
+ * pd candidate review --candidate-id <id> [--workspace <path>] [--json]
9
+ * pd candidate review --candidate-id <id> --decide reuse --principle-id <id> --reason "..." [--json]
10
+ * pd candidate review --candidate-id <id> --decide create --reason "..." [--json]
8
11
  * pd candidate audit --workspace <path> [--json]
9
12
  * pd candidate repair --candidate-id <id> --workspace <path> [--json]
10
13
  * pd candidate route --candidate-id <id> --workspace <path> [--json]
11
14
  */
12
15
  import { randomUUID } from 'crypto';
13
16
  import * as path from 'path';
14
- import { RuntimeStateManager, SqliteConnection, candidateList, candidateShow, CandidateIntakeService, CandidateIntakeError, INTAKE_ERROR_CODES, decideInternalizationRoute, buildDreamerSeedFromCandidate, findExistingDreamerTask, parseSeedSourcePainId, PrincipleTreeLedgerAdapter, } from '@principles/core/runtime-v2';
17
+ import { RuntimeStateManager, SqliteConnection, candidateList, candidateShow, CandidateIntakeService, CandidateIntakeError, INTAKE_ERROR_CODES, decideInternalizationRoute, buildDreamerSeedFromCandidate, findExistingDreamerTask, parseSeedSourcePainId, PrincipleTreeLedgerAdapter,
18
+ // PRI-917 PR3A: the review surface consumes the SAME proposal/semantics
19
+ // machinery as the intake gate (audit C4 — one implementation, no drift).
20
+ buildReuseProposal, buildReuseShortlist, extractIntakeRecommendation, isPrincipleLedgerEligibleKind, resolveOwnerIdentity, defaultOwnerIdentityHomeDir, ReuseEvaluationRunner, ReuseEvaluationError, storeEmitter, } from '@principles/core/runtime-v2';
15
21
  import { loadLedger, getLedgerFilePathPublic } from '@principles/core/principle-tree-ledger';
16
22
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
23
+ import { loadPdConfig } from '../services/pd-config-loader.js';
24
+ import { resolveRuntimeAdapterFromConfig } from '../services/runtime-adapter-resolver.js';
17
25
  import { resolvePromptFullPipelineSeedMode } from '../services/pd-config-loader.js';
18
26
  import { createRemediationResult, remediationAction } from './remediation-output.js';
19
27
  import { checkAdmissionGate } from './admission-gate.js';
@@ -380,6 +388,243 @@ export async function handleCandidateShow(opts) {
380
388
  }
381
389
  }
382
390
  // ── Intake ───────────────────────────────────────────────────────────────────
391
+ // ── Shared intake/decision finalizers (PRI-917 PR3A) ─────────────────────────
392
+ /**
393
+ * PRI-917 PR3A (surface audit C2): unify the candidate terminal state after a
394
+ * successful ledger write OR a recorded reuse resolution — always through the
395
+ * guarded {@link updateCandidateStatus} (optimistic pending→consumed +
396
+ * consumed_at), never bypassing the existing mechanism (SPEC §13: the
397
+ * candidate still reaches `consumed`).
398
+ *
399
+ * Returns 'already_consumed' when the candidate was consumed before this call
400
+ * (e.g. an INV-R07 replay). Exits non-zero on a guarded-transition failure —
401
+ * the ledger/LEDGER fact is already durable, so the inconsistency must be
402
+ * loud (same posture as the pre-PR3A intake path).
403
+ */
404
+ async function markCandidateConsumed(stateManager, candidateId, ledgerFact) {
405
+ const candidate = await stateManager.getCandidate(candidateId);
406
+ if (candidate?.status === 'consumed') {
407
+ return 'already_consumed';
408
+ }
409
+ try {
410
+ await updateCandidateStatus({ stateManager, candidateId, targetStatus: 'consumed', expectedCurrentStatus: 'pending' });
411
+ }
412
+ catch (err) {
413
+ const msg = `The ledger fact is durable (${ledgerFact}) but the candidate status update failed: ${err instanceof Error ? err.message : String(err)}. ` +
414
+ `Candidate ${candidateId} may be in inconsistent state.`;
415
+ console.error(`ERROR: ${msg}`);
416
+ process.exit(1);
417
+ }
418
+ return 'consumed';
419
+ }
420
+ /**
421
+ * PRI-917 PR3A (surface audit C1/C2): report a `reuse_selected` intake result
422
+ * as the SUCCESS it is — resolution + selected Principle + evidence, exit 0 —
423
+ * and unify the candidate terminal state (C2). Used by BOTH `pd candidate
424
+ * intake` (replay path) and `pd candidate review --decide reuse` (fresh
425
+ * decision). The CLI prints the durable fact; it does not create one.
426
+ */
427
+ async function reportReuseResolution(opts) {
428
+ const { stateManager, candidateId, result, json } = opts;
429
+ if (!result.selectedPrincipleId || !result.reuseEvidence) {
430
+ // Adapter/service contract violation — never print a hollow resolution.
431
+ throw new CandidateIntakeError(INTAKE_ERROR_CODES.INPUT_INVALID, `reuse_selected result for candidate ${candidateId} is missing selectedPrincipleId/reuseEvidence (contract violation)`, { candidateId, reason: 'reuse_resolution_contract_violation' });
432
+ }
433
+ const consumedState = await markCandidateConsumed(stateManager, candidateId, `reuse evidence on ${result.selectedPrincipleId}`);
434
+ const replayed = result.reuseProposal === undefined;
435
+ const resolution = {
436
+ candidateId,
437
+ status: 'reused',
438
+ selectedPrincipleId: result.selectedPrincipleId,
439
+ reuseEvidence: result.reuseEvidence,
440
+ replayed,
441
+ candidateStatus: consumedState,
442
+ message: result.message,
443
+ };
444
+ if (json) {
445
+ console.log(JSON.stringify(resolution, null, 2));
446
+ return;
447
+ }
448
+ console.log(`\nPrinciple Candidate Reuse: ${candidateId}\n`);
449
+ console.log(` Candidate: ${candidateId}`);
450
+ console.log(` Reused Principle: ${result.selectedPrincipleId}`);
451
+ console.log(` Pain: ${result.reuseEvidence.painId}`);
452
+ console.log(` Decided By: ${result.reuseEvidence.actor.kind} ${result.reuseEvidence.actor.id}`);
453
+ console.log(` Decided At: ${result.reuseEvidence.decidedAt}`);
454
+ console.log(` Reason: ${result.reuseEvidence.reason}`);
455
+ console.log(` Candidate Status: ${consumedState === 'already_consumed' ? 'already consumed (INV-R07 replay)' : 'consumed'}`);
456
+ console.log(' Evidence: recorded on the Principle\'s reuseEvidence (append-only)\n');
457
+ }
458
+ async function evaluateReuseProposal(workspaceDir, claim, entries) {
459
+ const configLoad = loadPdConfig(workspaceDir);
460
+ const effective = configLoad.ok ? configLoad.effective : configLoad.defaults;
461
+ const capability = effective.config.reuseEvaluation;
462
+ if (!capability?.enabled) {
463
+ return null;
464
+ }
465
+ if (entries.length === 0) {
466
+ // Case A (SPEC §6): nothing credible was found — there is nothing to
467
+ // evaluate, and intake proceeds to create without an Owner decision.
468
+ return null;
469
+ }
470
+ // PRI-917 review (P2): timeout precedence — capability.timeoutMs wins,
471
+ // otherwise the resolved PROFILE's own timeout (its documented default),
472
+ // otherwise the runner's built-in default.
473
+ const capabilityProfile = effective.config.runtimeProfiles[capability.runtimeProfile ?? ''];
474
+ const profileTimeoutMs = capabilityProfile?.type === 'pi-ai' ? capabilityProfile.timeoutMs : undefined;
475
+ try {
476
+ const adapter = resolveRuntimeAdapterFromConfig({
477
+ runtimeKind: 'config',
478
+ workspaceDir,
479
+ runtimeProfileId: capability.runtimeProfile,
480
+ });
481
+ const runner = new ReuseEvaluationRunner({ runtimeAdapter: adapter }, { ...(capability.timeoutMs !== undefined ? { timeoutMs: capability.timeoutMs } : { ...(profileTimeoutMs !== undefined ? { timeoutMs: profileTimeoutMs } : {}) }) });
482
+ const output = await runner.recommend({
483
+ candidate: claim,
484
+ candidates: entries.map((entry) => ({
485
+ principleId: entry.principleId,
486
+ text: entry.existingPrinciple.text,
487
+ triggerPattern: entry.existingPrinciple.triggerPattern,
488
+ action: entry.existingPrinciple.action,
489
+ })),
490
+ });
491
+ // PRI-917 review (P2): the validator deliberately does NOT check id
492
+ // existence (INV-R08 lives one layer up, against the REAL shortlist).
493
+ // This is that layer: a hallucinated/foreign id must not reach the Owner
494
+ // as an executable-looking recommendation.
495
+ if (output.recommendation === 'reuse') {
496
+ const inShortlist = entries.some((entry) => entry.principleId === output.selectedPrincipleId);
497
+ if (!inShortlist) {
498
+ return {
499
+ status: 'unavailable',
500
+ reason: `selected_principle_not_in_shortlist:${output.selectedPrincipleId ?? '(absent)'}`,
501
+ };
502
+ }
503
+ }
504
+ return {
505
+ status: 'recommended',
506
+ recommendation: output.recommendation,
507
+ ...(output.selectedPrincipleId !== undefined ? { selectedPrincipleId: output.selectedPrincipleId } : {}),
508
+ rationale: output.rationale,
509
+ confidence: output.confidence,
510
+ };
511
+ }
512
+ catch (err) {
513
+ const reason = err instanceof ReuseEvaluationError
514
+ ? `${err.reason}: ${err.detail ?? err.message}`
515
+ : err instanceof Error
516
+ ? err.message
517
+ : String(err);
518
+ return { status: 'unavailable', reason };
519
+ }
520
+ }
521
+ /**
522
+ * Decision mode of {@link handleCandidateReview}: validate the CLI contract
523
+ * (rc-3), resolve the Owner identity (fail-closed), then run the EXISTING
524
+ * intake chain with the §11 verdict injected. No CLI-side persistence.
525
+ */
526
+ async function executeOwnerDecision(deps) {
527
+ const { opts, stateManager, ledgerAdapter, stateDir } = deps;
528
+ const failCli = (message, nextAction) => {
529
+ if (opts.json) {
530
+ console.log(JSON.stringify({ candidateId: opts.candidateId, status: 'refused', decision: opts.decide, error: message, nextAction }, null, 2));
531
+ }
532
+ else {
533
+ console.error(`ERROR: ${message}`);
534
+ console.error(` Next Action: ${nextAction}`);
535
+ }
536
+ process.exit(1);
537
+ };
538
+ if (typeof opts.reason !== 'string' || opts.reason.trim() === '') {
539
+ failCli('--reason is required with --decide (the evidence entry must record why the Owner resolved this candidate)', 'Re-run with --reason "..." (non-empty).');
540
+ }
541
+ if (opts.decide === 'reuse' && (typeof opts.principleId !== 'string' || opts.principleId.trim() === '')) {
542
+ failCli('--principle-id is required with --decide reuse', 'Re-run with --principle-id <id from the review proposal>.');
543
+ }
544
+ // Owner identity: env > ~/.pd/owner.json > none (ADR-0022 resolver).
545
+ const identity = resolveOwnerIdentity(process.env, defaultOwnerIdentityHomeDir());
546
+ if ((identity.source !== 'env' && identity.source !== 'file') || !identity.ownerId) {
547
+ failCli(`Owner identity is required to record a ${opts.decide} decision (identity source: ${identity.source})${identity.error ? `: ${identity.error}` : ''}`, 'Register the Owner identity file (~/.pd/owner.json) or set PD_OWNER_ID and PD_OWNER_CREDENTIAL_ID together, then retry.');
548
+ }
549
+ const decision = opts.decide === 'reuse'
550
+ ? {
551
+ decision: 'reuse',
552
+ selectedPrincipleId: opts.principleId,
553
+ actor: { kind: 'owner', id: identity.ownerId },
554
+ reason: opts.reason,
555
+ decidedAt: new Date().toISOString(),
556
+ }
557
+ : { decision: 'create' };
558
+ // INV-R08: verify the selected principle is in the current proposal.
559
+ // This prevents a stale or empty proposal from silently degrading reuse
560
+ // into create.
561
+ if (opts.decide === 'reuse') {
562
+ const candidate = await stateManager.getCandidate(opts.candidateId);
563
+ const artifact = candidate ? await stateManager.getArtifact(candidate.artifactId) : null;
564
+ const extracted = candidate ? extractIntakeRecommendation(candidate, artifact) : null;
565
+ if (!candidate || !extracted || !extracted.ok) {
566
+ failCli('Cannot build the reuse proposal for this candidate', 'Run `pd candidate review --candidate-id <id>` first.');
567
+ return;
568
+ }
569
+ const proposal = buildReuseProposal(opts.candidateId, buildReuseShortlist({
570
+ text: extracted.recommendation.text || candidate.description || '',
571
+ triggerPattern: extracted.recommendation.triggerPattern ?? '',
572
+ action: extracted.recommendation.action ?? '',
573
+ }, stateDir, { recommendationKind: candidate.rawRecommendationKind }));
574
+ if (!proposal.candidates.some((c) => c.principleId === opts.principleId)) {
575
+ failCli(`Principle ${opts.principleId} is not in the current reuse proposal`, 'Re-run `pd candidate review` to see the current proposal.');
576
+ }
577
+ }
578
+ const service = new CandidateIntakeService({
579
+ stateManager,
580
+ ledgerAdapter,
581
+ // The verdict is fixed by the CLI invocation; the intake gate still
582
+ // validates it (rc-1/rc-2/rc-3) and checks the selection against the
583
+ // proposal (INV-R08) — the CLI cannot smuggle a decision past the gate.
584
+ reuseDecision: () => decision,
585
+ reuseStateDir: stateDir,
586
+ });
587
+ const intakeResult = await service.intake(opts.candidateId);
588
+ if (intakeResult.outcome === 'refused' && intakeResult.reason === 'reuse_selected') {
589
+ await reportReuseResolution({ stateManager, candidateId: opts.candidateId, result: intakeResult, json: opts.json === true });
590
+ return;
591
+ }
592
+ if (intakeResult.outcome === 'refused') {
593
+ // A non-principle kind or unknown kind cannot carry a reuse decision.
594
+ throw new CandidateIntakeError(INTAKE_ERROR_CODES.INPUT_INVALID, `Principle Ledger write refused: ${intakeResult.message}`, { candidateId: opts.candidateId, reason: intakeResult.reason });
595
+ }
596
+ // create verdict (or gate absent shapes) — same durable outcome as intake.
597
+ const { entry } = intakeResult;
598
+ const consumedState = await markCandidateConsumed(stateManager, opts.candidateId, entry.id);
599
+ if (consumedState === 'already_consumed') {
600
+ // Consumed before this call — the ledger entry is identical by intake
601
+ // idempotency; report it like `pd candidate intake` does.
602
+ const infoMessage = `Candidate ${opts.candidateId} was already consumed. Ledger entry: ${entry.id}`;
603
+ if (opts.json) {
604
+ console.log(JSON.stringify({ candidateId: opts.candidateId, ledgerEntryId: entry.id, status: 'already_consumed', message: infoMessage }, null, 2));
605
+ }
606
+ else {
607
+ console.log(infoMessage);
608
+ }
609
+ return;
610
+ }
611
+ const result = {
612
+ candidateId: opts.candidateId,
613
+ decision: opts.decide,
614
+ ledgerEntryId: entry.id,
615
+ status: 'consumed',
616
+ };
617
+ if (opts.json) {
618
+ console.log(JSON.stringify(result, null, 2));
619
+ return;
620
+ }
621
+ console.log(`\nPrinciple Candidate Decision: ${opts.candidateId}\n`);
622
+ console.log(` Decision: ${opts.decide}`);
623
+ console.log(` Candidate: ${opts.candidateId}`);
624
+ console.log(` Ledger Entry: ${entry.id}`);
625
+ console.log(` Status: consumed\n`);
626
+ console.log('Decision complete.\n');
627
+ }
383
628
  /**
384
629
  * pd candidate intake --candidate-id <id> [--workspace <path>] [--json] [--dry-run]
385
630
  *
@@ -465,12 +710,22 @@ export async function handleCandidateIntake(opts) {
465
710
  // candidate id, so a Principle Ledger write boundary refusal is surfaced
466
711
  // loudly (never silently swallowed) instead of being treated as success.
467
712
  const intakeResult = await service.intake(opts.candidateId);
713
+ if (intakeResult.outcome === 'refused' && intakeResult.reason === 'reuse_selected') {
714
+ // PRI-917 PR3A (surface audit C1/C2): a recorded reuse resolution is a
715
+ // SUCCESS, not a failure — the evidence is already durable on the
716
+ // selected Principle (fresh decision) or was recorded earlier (INV-R07
717
+ // replay via intake step 2b, which is how this branch is reachable from
718
+ // the gate-less intake command). Report the resolution, unify the
719
+ // candidate terminal state with the automatic paths (SPEC §13), exit 0.
720
+ await reportReuseResolution({ stateManager, candidateId: opts.candidateId, result: intakeResult, json: opts.json === true });
721
+ return;
722
+ }
468
723
  if (intakeResult.outcome === 'refused') {
469
724
  throw new CandidateIntakeError(INTAKE_ERROR_CODES.INPUT_INVALID, `Principle Ledger write refused: ${intakeResult.message}`, { candidateId: opts.candidateId, reason: intakeResult.reason });
470
725
  }
471
726
  const { entry } = intakeResult;
472
- // Check if already consumed before this call
473
- if (candidate?.status === 'consumed') {
727
+ const consumedState = await markCandidateConsumed(stateManager, opts.candidateId, entry.id);
728
+ if (consumedState === 'already_consumed') {
474
729
  const infoMessage = `Candidate ${opts.candidateId} was already consumed. Ledger entry: ${entry.id}`;
475
730
  if (opts.json) {
476
731
  console.log(JSON.stringify({
@@ -485,16 +740,6 @@ export async function handleCandidateIntake(opts) {
485
740
  }
486
741
  return;
487
742
  }
488
- // Update DB status — this must succeed; if it fails, exit non-zero
489
- try {
490
- await updateCandidateStatus({ stateManager, candidateId: opts.candidateId, targetStatus: 'consumed', expectedCurrentStatus: 'pending' });
491
- }
492
- catch (err) {
493
- const msg = `Ledger write succeeded (entry ${entry.id}) but DB status update failed: ${err instanceof Error ? err.message : String(err)}. ` +
494
- `Candidate ${opts.candidateId} may be in inconsistent state.`;
495
- console.error(`ERROR: ${msg}`);
496
- process.exit(1);
497
- }
498
743
  const result = {
499
744
  candidateId: opts.candidateId,
500
745
  ledgerEntryId: entry.id,
@@ -526,6 +771,255 @@ export async function handleCandidateIntake(opts) {
526
771
  await stateManager.close();
527
772
  }
528
773
  }
774
+ /**
775
+ * pd candidate review — PRI-917 PR3A Owner decision surface (SPEC §18).
776
+ *
777
+ * Phase 1 (read-only): shows the candidate and the deterministic Top-K reuse
778
+ * proposal (score, reasons/shared terms, and the existing Principle's text /
779
+ * triggerPattern / action / status) so the Owner can decide with the same
780
+ * information the gate will consume.
781
+ *
782
+ * Phase 2 (`--decide`): constructs the SPEC §11 verdict (actor from
783
+ * resolveOwnerIdentity — fail-closed when unresolvable) and injects it into
784
+ * CandidateIntakeService, which runs the EXISTING gate → appendReuseEvidence
785
+ * chain. The CLI is an interaction layer only: it never writes reuseEvidence,
786
+ * decision files, or any other state (SPEC §18 — the only durable authority
787
+ * is `Principle.reuseEvidence[]`).
788
+ */
789
+ export async function handleCandidateReview(opts) {
790
+ const workspaceDir = resolveWorkspaceDir(opts.workspace);
791
+ const stateManager = new RuntimeStateManager({ workspaceDir });
792
+ try {
793
+ await stateManager.initialize();
794
+ const stateDir = path.join(workspaceDir, '.state');
795
+ const ledgerAdapter = new PrincipleTreeLedgerAdapter({ stateDir });
796
+ const candidate = await stateManager.getCandidate(opts.candidateId);
797
+ if (!candidate) {
798
+ console.error(`Candidate not found: ${opts.candidateId}`);
799
+ process.exit(1);
800
+ return;
801
+ }
802
+ if (opts.decide === 'reuse' || opts.decide === 'create') {
803
+ // PRI-442 Stage 4: admission gate check — same as handleCandidateIntake.
804
+ // Prevents CLI from bypassing the production admission gate.
805
+ //
806
+ // PRI-917 v0.3.3 (R5 F4 two-heads-block fix): the pre-check does NOT
807
+ // apply to a `reuse` verdict. Admission confidence protects the LEDGER
808
+ // from low-quality NEW principles; a reuse decision creates nothing — it
809
+ // appends one auditable evidence entry to an EXISTING principle whose
810
+ // recurrence is itself the proof of value. Blocking it here left every
811
+ // low-confidence historical candidate permanently undecidable while the
812
+ // auto path consumed high-confidence ones before any Owner could ask.
813
+ // `create` keeps the pre-check: a low-confidence candidate still cannot
814
+ // become a new Principle via the CLI.
815
+ if (opts.decide === 'create') {
816
+ const admissionBlock = checkAdmissionGate(candidate);
817
+ if (admissionBlock) {
818
+ const payload = {
819
+ candidateId: opts.candidateId,
820
+ status: 'refused',
821
+ admissionDecision: admissionBlock.decision,
822
+ reason: admissionBlock.reason,
823
+ nextAction: admissionBlock.nextAction,
824
+ };
825
+ if (opts.json) {
826
+ console.log(JSON.stringify(payload, null, 2));
827
+ }
828
+ else {
829
+ console.error(`Admission gate refused candidate ${opts.candidateId}: ${admissionBlock.decision}`);
830
+ console.error(` Reason: ${admissionBlock.reason}`);
831
+ console.error(` Next Action: ${admissionBlock.nextAction}`);
832
+ }
833
+ process.exit(1);
834
+ return;
835
+ }
836
+ }
837
+ // opts.decide is narrowed here — no cast needed to build the decision-mode shape.
838
+ await executeOwnerDecision({ opts: { ...opts, decide: opts.decide }, stateManager, ledgerAdapter, stateDir });
839
+ return;
840
+ }
841
+ // ── Read-only proposal view ──
842
+ const rawKindEligible = isPrincipleLedgerEligibleKind(candidate.rawRecommendationKind);
843
+ if (!rawKindEligible) {
844
+ // Fail closed like the intake boundary: a non-principle candidate never
845
+ // reaches the reuse gate, so reviewing a proposal for it would lie.
846
+ const rawKind = typeof candidate.rawRecommendationKind === 'string' ? candidate.rawRecommendationKind : null;
847
+ const payload = {
848
+ candidateId: opts.candidateId,
849
+ recommendationKindEligible: false,
850
+ rawRecommendationKind: rawKind,
851
+ message: `Candidate ${opts.candidateId} has recommendation_kind '${rawKind ?? 'absent'}' which does not target the Principle Ledger; there is no reuse proposal to review.`,
852
+ nextAction: 'Run `pd candidate route --candidate-id <id>` for this candidate\'s internalization route instead.',
853
+ };
854
+ if (opts.json) {
855
+ console.log(JSON.stringify(payload, null, 2));
856
+ return;
857
+ }
858
+ console.log(`\nPrinciple Candidate Review: ${opts.candidateId}\n`);
859
+ console.log(` Kind: ${rawKind ?? 'absent'} (not Principle-Ledger eligible)`);
860
+ console.log(` ${payload.message}`);
861
+ console.log(` Next Action: ${payload.nextAction}\n`);
862
+ return;
863
+ }
864
+ const artifact = await stateManager.getArtifact(candidate.artifactId);
865
+ if (!artifact) {
866
+ console.error(`Artifact not found for candidate: ${opts.candidateId} (artifact ${candidate.artifactId})`);
867
+ process.exit(1);
868
+ return;
869
+ }
870
+ // C4: the SAME extraction intake uses (4b/4c) — never a second parser.
871
+ const extracted = extractIntakeRecommendation(candidate, artifact);
872
+ if (!extracted.ok) {
873
+ throw extracted.error;
874
+ }
875
+ const { recommendation } = extracted;
876
+ const shortlist = buildReuseShortlist({
877
+ text: recommendation.text || candidate.description || '',
878
+ triggerPattern: recommendation.triggerPattern ?? '',
879
+ action: recommendation.action ?? '',
880
+ }, stateDir, { recommendationKind: candidate.rawRecommendationKind });
881
+ const proposal = buildReuseProposal(opts.candidateId, shortlist);
882
+ // Enrich each entry with the existing Principle's durable content.
883
+ const ledger = loadLedger(stateDir);
884
+ const entries = proposal.candidates.map((entry) => {
885
+ const principle = ledger.tree.principles[entry.principleId];
886
+ return {
887
+ principleId: entry.principleId,
888
+ score: entry.score,
889
+ reasons: entry.reasons,
890
+ existingPrinciple: principle
891
+ ? { text: principle.text, triggerPattern: principle.triggerPattern, action: principle.action, status: principle.status }
892
+ : { text: '', triggerPattern: '', action: '', status: 'missing' },
893
+ };
894
+ });
895
+ // PRI-917 v0.3.2 (Phase 3C-4): run the Semantic Reuse Evaluation
896
+ // Capability as a DISPLAY enhancement on the read-only review path. Its
897
+ // output is a RECOMMENDATION for the Owner — it never decides, never
898
+ // writes, and its failure degrades observably to lexical-only (SPEC §9).
899
+ const evaluation = await evaluateReuseProposal(workspaceDir, {
900
+ text: recommendation.text || candidate.description || '',
901
+ triggerPattern: recommendation.triggerPattern ?? '',
902
+ action: recommendation.action ?? '',
903
+ }, entries);
904
+ // PRI-917 v0.3.2 §11: the events are OBSERVATION ONLY. They record what
905
+ // the capability recommended; they are never decision storage (the
906
+ // Owner's verdict lives exclusively in Principle.reuseEvidence[]) and
907
+ // no intake outcome reads them. Emitted only when the capability
908
+ // actually ran (disabled or nothing-to-evaluate emits nothing).
909
+ if (evaluation !== null) {
910
+ const timestamp = new Date().toISOString();
911
+ if (evaluation.status === 'recommended') {
912
+ storeEmitter.emitTelemetry({
913
+ eventType: 'reuse_evaluation_recommended',
914
+ traceId: opts.candidateId,
915
+ timestamp,
916
+ sessionId: 'pd-cli-review',
917
+ agentId: 'reuse-evaluation',
918
+ payload: {
919
+ candidateId: opts.candidateId,
920
+ recommendation: evaluation.recommendation,
921
+ ...(evaluation.selectedPrincipleId !== undefined ? { selectedPrincipleId: evaluation.selectedPrincipleId } : {}),
922
+ confidence: evaluation.confidence,
923
+ },
924
+ });
925
+ }
926
+ else {
927
+ storeEmitter.emitTelemetry({
928
+ eventType: 'reuse_evaluation_unavailable',
929
+ traceId: opts.candidateId,
930
+ timestamp,
931
+ sessionId: 'pd-cli-review',
932
+ agentId: 'reuse-evaluation',
933
+ payload: {
934
+ candidateId: opts.candidateId,
935
+ reason: evaluation.reason.slice(0, 200),
936
+ },
937
+ });
938
+ }
939
+ }
940
+ const nextAction = entries.length > 0
941
+ ? `pd candidate review --candidate-id ${opts.candidateId} --decide reuse --principle-id <id> --reason "..." (or --decide create --reason "...")`
942
+ : `No credible reuse proposal — intake will create a new Principle: pd candidate intake --candidate-id ${opts.candidateId} --workspace "${workspaceDir}"`;
943
+ if (opts.json) {
944
+ console.log(JSON.stringify({
945
+ candidateId: opts.candidateId,
946
+ title: candidate.title,
947
+ description: candidate.description,
948
+ status: candidate.status,
949
+ taskId: candidate.taskId,
950
+ recommendationKindEligible: true,
951
+ claim: { text: recommendation.text || candidate.description || '', triggerPattern: recommendation.triggerPattern ?? '', action: recommendation.action ?? '' },
952
+ proposal: {
953
+ status: proposal.status,
954
+ eligibleCount: proposal.eligibleCount,
955
+ candidates: entries.map((entry) => ({
956
+ ...entry,
957
+ semanticMatch: evaluation?.status === 'recommended'
958
+ && evaluation.recommendation === 'reuse'
959
+ && evaluation.selectedPrincipleId === entry.principleId,
960
+ })),
961
+ },
962
+ ...(evaluation ? { evaluation } : {}),
963
+ nextAction,
964
+ }, null, 2));
965
+ return;
966
+ }
967
+ console.log(`\nPrinciple Candidate Review: ${opts.candidateId}\n`);
968
+ console.log(` Title: ${candidate.title}`);
969
+ console.log(` Description: ${candidate.description}`);
970
+ console.log(` Status: ${candidate.status}`);
971
+ console.log(` Task: ${candidate.taskId}`);
972
+ console.log(` Claim: ${recommendation.text || candidate.description || ''}`);
973
+ console.log(` Trigger: ${recommendation.triggerPattern ?? ''}`);
974
+ console.log(` Action: ${recommendation.action ?? ''}`);
975
+ console.log(`\n Reuse Proposal (${entries.length} of ${proposal.eligibleCount} eligible, Top-3):\n`);
976
+ if (entries.length === 0) {
977
+ console.log(' No credible existing Principle found — the intake gate will create a new one.\n');
978
+ }
979
+ entries.forEach((entry, index) => {
980
+ console.log(` ${index + 1}. ${entry.principleId} (score ${entry.score.toFixed(3)})`);
981
+ for (const reason of entry.reasons) {
982
+ console.log(` reason: ${reason}`);
983
+ }
984
+ console.log(` text: ${entry.existingPrinciple.text}`);
985
+ console.log(` trigger: ${entry.existingPrinciple.triggerPattern}`);
986
+ console.log(` action: ${entry.existingPrinciple.action}`);
987
+ console.log(` status: ${entry.existingPrinciple.status}`);
988
+ });
989
+ console.log('');
990
+ if (evaluation === null) {
991
+ // Capability disabled (or nothing to evaluate): lexical-only review,
992
+ // exactly the pre-3C-4 output.
993
+ }
994
+ else if (evaluation.status === 'recommended') {
995
+ console.log(' Semantic evaluation (advisory — the Owner decides):');
996
+ console.log(` recommendation: ${evaluation.recommendation}`);
997
+ if (evaluation.selectedPrincipleId) {
998
+ console.log(` principle: ${evaluation.selectedPrincipleId}`);
999
+ }
1000
+ console.log(` rationale: ${evaluation.rationale}`);
1001
+ console.log(` confidence: ${evaluation.confidence}`);
1002
+ }
1003
+ else {
1004
+ console.log(' Semantic evaluation: UNAVAILABLE (lexical proposal only — not semantically judged)');
1005
+ console.log(` reason: ${evaluation.reason}`);
1006
+ }
1007
+ console.log(`\n Next Action: ${nextAction}\n`);
1008
+ }
1009
+ catch (err) {
1010
+ if (err instanceof CandidateIntakeError || err.name === 'CandidateIntakeError') {
1011
+ const e = err;
1012
+ console.error(`Review failed [${e.code ?? 'unknown'}]: ${e.message}`);
1013
+ }
1014
+ else {
1015
+ console.error(`Review failed: ${String(err)}`);
1016
+ }
1017
+ process.exit(1);
1018
+ }
1019
+ finally {
1020
+ await stateManager.close();
1021
+ }
1022
+ }
529
1023
  // ── Audit ─────────────────────────────────────────────────────────────────────
530
1024
  /**
531
1025
  * pd candidate audit --workspace <path> [--json]