@allocator-one/rcl 4.5.7 → 4.5.9

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 (84) hide show
  1. package/README.md +5 -4
  2. package/dist/consensus/semantic-deduper.d.ts +2 -1
  3. package/dist/consensus/semantic-deduper.js +21 -10
  4. package/dist/consensus/semantic-deduper.js.map +1 -1
  5. package/dist/converge/attempt-budget.d.ts +11 -0
  6. package/dist/converge/attempt-budget.js +119 -61
  7. package/dist/converge/attempt-budget.js.map +1 -1
  8. package/dist/converge/delivery-reconciliation.d.ts +2 -1
  9. package/dist/converge/delivery-reconciliation.js +17 -9
  10. package/dist/converge/delivery-reconciliation.js.map +1 -1
  11. package/dist/converge/guarded-input-retention.d.ts +23 -0
  12. package/dist/converge/guarded-input-retention.js +179 -0
  13. package/dist/converge/guarded-input-retention.js.map +1 -0
  14. package/dist/converge/launch-guard.d.ts +3 -0
  15. package/dist/converge/launch-guard.js +53 -17
  16. package/dist/converge/launch-guard.js.map +1 -1
  17. package/dist/converge/ordinary-pending-export.js +74 -40
  18. package/dist/converge/ordinary-pending-export.js.map +1 -1
  19. package/dist/converge/ordinary-pending-package.d.ts +16 -2
  20. package/dist/converge/ordinary-pending-package.js +77 -4
  21. package/dist/converge/ordinary-pending-package.js.map +1 -1
  22. package/dist/converge/recovered-production.d.ts +14 -0
  23. package/dist/converge/recovered-production.js +61 -0
  24. package/dist/converge/recovered-production.js.map +1 -0
  25. package/dist/converge/recovery-launch.js +82 -35
  26. package/dist/converge/recovery-launch.js.map +1 -1
  27. package/dist/converge/run-state.d.ts +2 -0
  28. package/dist/converge/run-state.js +26 -12
  29. package/dist/converge/run-state.js.map +1 -1
  30. package/dist/converge/semantic-state.js +18 -4
  31. package/dist/converge/semantic-state.js.map +1 -1
  32. package/dist/coordination/registry-lock.d.ts +2 -0
  33. package/dist/coordination/registry-lock.js +42 -13
  34. package/dist/coordination/registry-lock.js.map +1 -1
  35. package/dist/evidence/claim-recovery/delivery.d.ts +25 -0
  36. package/dist/evidence/claim-recovery/delivery.js +100 -0
  37. package/dist/evidence/claim-recovery/delivery.js.map +1 -0
  38. package/dist/evidence/claim-recovery/public-adoption.d.ts +31 -0
  39. package/dist/evidence/claim-recovery/public-adoption.js +177 -0
  40. package/dist/evidence/claim-recovery/public-adoption.js.map +1 -0
  41. package/dist/evidence/claim-recovery/public-manifest.d.ts +114 -0
  42. package/dist/evidence/claim-recovery/public-manifest.js +11 -0
  43. package/dist/evidence/claim-recovery/public-manifest.js.map +1 -0
  44. package/dist/evidence/claim-recovery/public-model.d.ts +85 -0
  45. package/dist/evidence/claim-recovery/public-model.js +177 -0
  46. package/dist/evidence/claim-recovery/public-model.js.map +1 -0
  47. package/dist/evidence/original-run/journal.d.ts +10 -0
  48. package/dist/evidence/original-run/journal.js +55 -3
  49. package/dist/evidence/original-run/journal.js.map +1 -1
  50. package/dist/evidence/original-run/lock.js +22 -9
  51. package/dist/evidence/original-run/lock.js.map +1 -1
  52. package/dist/evidence/recover-claim.d.ts +21 -0
  53. package/dist/evidence/recover-claim.js +504 -0
  54. package/dist/evidence/recover-claim.js.map +1 -0
  55. package/dist/index.js +234 -139
  56. package/dist/index.js.map +1 -1
  57. package/dist/report/assembly.d.ts +2 -0
  58. package/dist/report/assembly.js +1 -0
  59. package/dist/report/assembly.js.map +1 -1
  60. package/dist/report/consensus-assembly.d.ts +2 -0
  61. package/dist/report/consensus-assembly.js +6 -3
  62. package/dist/report/consensus-assembly.js.map +1 -1
  63. package/dist/report/reviewer-artifact.js +3 -1
  64. package/dist/report/reviewer-artifact.js.map +1 -1
  65. package/dist/report/run-header.js +2 -0
  66. package/dist/report/run-header.js.map +1 -1
  67. package/dist/telemetry/deliver.d.ts +8 -0
  68. package/dist/telemetry/deliver.js +55 -8
  69. package/dist/telemetry/deliver.js.map +1 -1
  70. package/dist/telemetry/events.js +2 -0
  71. package/dist/telemetry/events.js.map +1 -1
  72. package/dist/telemetry/outbox.d.ts +20 -0
  73. package/dist/telemetry/outbox.js +324 -103
  74. package/dist/telemetry/outbox.js.map +1 -1
  75. package/dist/telemetry/reviewer-delivery.d.ts +95 -3
  76. package/dist/telemetry/reviewer-delivery.js +600 -12
  77. package/dist/telemetry/reviewer-delivery.js.map +1 -1
  78. package/dist/telemetry/sink.d.ts +14 -0
  79. package/dist/telemetry/sink.js +44 -2
  80. package/dist/telemetry/sink.js.map +1 -1
  81. package/dist/telemetry/terminal-reviewer-delivery.d.ts +154 -5
  82. package/dist/telemetry/terminal-reviewer-delivery.js +196 -21
  83. package/dist/telemetry/terminal-reviewer-delivery.js.map +1 -1
  84. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -47,7 +47,9 @@ import { resolveFinding, buildDiscussPrompts, runDiscussion, loadContextDocs, }
47
47
  import { DEFAULT_CONVERGE_ATTEMPT_CAP, claimConvergeAttempt, ConvergeAttemptBudgetExceededError, convergeAttemptErrorExitCode, ConvergeAttemptStateError, resolveGitCommonDir, } from './converge/attempt-budget.js';
48
48
  import { DEFAULT_CONVERGE_ROUND_CAP, HARD_CONVERGE_ROUND_CAP, processRoundReport, recordVerdicts, findingGatingReason, ConvergeRoundCapError, ConvergeRunStateError, } from './converge/run-state.js';
49
49
  import { applyRoundGap, previewRoundGap } from './converge/round-gap.js';
50
+ import { withNativeTarget } from './converge/target-ownership.js';
50
51
  import { guardReviewLaunch, ReviewLaunchRefused } from './converge/launch-guard.js';
52
+ import { selectCurrentRecoveredProduction } from './converge/recovered-production.js';
51
53
  import { createBoundFixRecovery } from './converge/bound-fix-recovery.js';
52
54
  import { openReadSink } from './telemetry/read-sink.js';
53
55
  import { reconcileFlushedRun } from './converge/delivery-reconciliation.js';
@@ -79,6 +81,7 @@ import { parseRepoName } from './evidence/target.js';
79
81
  import { text } from './evidence/format.js';
80
82
  import { loadConvergeRunState } from './converge/run-state.js';
81
83
  import { finalizeOrdinaryPendingLaunch, previewOrdinaryPendingLaunch, resumePendingLegacyLaunch } from './converge/pending-legacy-resume.js';
84
+ import { ordinaryPendingGuardedInput } from './converge/ordinary-pending-package.js';
82
85
  import { exportOrdinaryPendingPackage, retainOrdinaryLaunchInputs } from './converge/ordinary-pending-export.js';
83
86
  import { capturePreparedCouncil } from './dispatch/capture-council.js';
84
87
  import { executeCapturedOriginal } from './dispatch/original-execution.js';
@@ -109,7 +112,7 @@ program.hook('preAction', async (_thisCommand, actionCommand) => {
109
112
  // Reads and explicit repairs must not flush unrelated evidence, even in preview.
110
113
  if (actionCommand.parent?.name() === 'evidence' && (name === 'show' || name === 'status'))
111
114
  return;
112
- if (name === 'converge-rejected' || name === 'converge-stale' || name === 'converge-gap' || name === 'converge-reconcile-history' || name === 'recover-run' || name === 'recover-finding' || name === 'retriage-finding' || name === 'telemetry' || actionCommand.parent?.name() === 'telemetry' || name.includes('worker'))
115
+ if (name === 'converge-rejected' || name === 'converge-stale' || name === 'converge-gap' || name === 'converge-reconcile-history' || name === 'recover-run' || name === 'recover-claim' || name === 'recover-finding' || name === 'retriage-finding' || name === 'telemetry' || actionCommand.parent?.name() === 'telemetry' || name.includes('worker'))
113
116
  return;
114
117
  const flags = actionCommand.opts();
115
118
  if (flags.telemetry === false || (process.env['RCL_TELEMETRY'] ?? '').trim().toLowerCase() === 'off')
@@ -233,7 +236,7 @@ program
233
236
  .option('--round <n>', 'Converge round number (or RCL_CONVERGE_ROUND)')
234
237
  .option('--attempt <n>', 'Converge attempt number (or RCL_CONVERGE_ATTEMPT)')
235
238
  .option('--start-over', 'Start an explicitly requested fresh review with a new normal budget; retain all prior evidence and spending')
236
- .option('--guarded-converge', 'Validate and claim inside this review process; derive the round from native state')
239
+ .option('--guarded-converge', 'Validate and claim inside this review process, or consume an exact recovered-v3 --attempt; derive the round from native state')
237
240
  .option('--bound-fix-recovery <run-id>', 'Review unchanged inputs once live Harness evidence proves a retained bound fix obligation after this native dismissal-only run')
238
241
  .option('--launch-intent <intent>', 'Guarded intent: review, stop-upstream, stop-review, or retry-delivery')
239
242
  .option('--retry-report <path>', 'Original legacy report proving an inconclusive launch; the new inputs may differ; requires --retry-reason')
@@ -328,24 +331,31 @@ program
328
331
  throw new ConvergeAttemptStateError('maxAttempts (--max-attempts) must be a positive safe integer.');
329
332
  }
330
333
  }
331
- const claim = await claimConvergeAttempt({
332
- gitCommonDir: await resolveGitCommonDir(),
333
- target: opts.target,
334
- maxAttempts,
334
+ const gitCommonDir = await resolveGitCommonDir();
335
+ const claim = await withNativeTarget(gitCommonDir, opts.target, async (ownership) => {
336
+ const committed = await claimConvergeAttempt({
337
+ gitCommonDir,
338
+ target: opts.target,
339
+ maxAttempts,
340
+ ownership,
341
+ });
342
+ await reportConvergeEvents([
343
+ buildEvent({
344
+ kind: 'attempt_claimed',
345
+ convergeTarget: committed.target,
346
+ attempt: committed.attempt,
347
+ // The claim's local state path and process id stay on this machine.
348
+ payload: { attempt: committed.attempt, cap: committed.cap,
349
+ ...(committed.cycle ? { cycle_id: committed.cycle.id } : {}) },
350
+ }),
351
+ // An explicit --max-attempts is consent evidence, whatever it was before.
352
+ ...(maxAttempts !== undefined
353
+ ? [buildEvent({ kind: 'cap_changed', convergeTarget: committed.target, attempt: committed.attempt,
354
+ payload: { kind: 'attempts', to: committed.cap } })]
355
+ : []),
356
+ ]);
357
+ return committed;
335
358
  });
336
- await reportConvergeEvents([
337
- buildEvent({
338
- kind: 'attempt_claimed',
339
- convergeTarget: claim.target,
340
- attempt: claim.attempt,
341
- // The claim's local state path and process id stay on this machine.
342
- payload: { attempt: claim.attempt, cap: claim.cap, ...(claim.cycle ? { cycle_id: claim.cycle.id } : {}) },
343
- }),
344
- // An explicit --max-attempts is consent evidence, whatever it was before.
345
- ...(maxAttempts !== undefined
346
- ? [buildEvent({ kind: 'cap_changed', convergeTarget: claim.target, attempt: claim.attempt, payload: { kind: 'attempts', to: claim.cap } })]
347
- : []),
348
- ]);
349
359
  if (opts.json) {
350
360
  console.log(JSON.stringify(claim));
351
361
  }
@@ -595,10 +605,12 @@ program
595
605
  }
596
606
  }
597
607
  let report;
608
+ let reportJson;
598
609
  let reportSha256;
599
610
  try {
600
611
  const source = await readFile(opts.report);
601
- report = JSON.parse(source.toString('utf8'));
612
+ reportJson = source.toString('utf8');
613
+ report = JSON.parse(reportJson);
602
614
  reportSha256 = sha256(source);
603
615
  }
604
616
  catch (err) {
@@ -625,48 +637,69 @@ program
625
637
  if (reportRunId !== undefined && runId === undefined) {
626
638
  console.error(chalk.yellow(`Report run ${reportRunId} ${reportTarget === undefined ? 'carries no converge target' : `belongs to converge target ${reportTarget}`}, not ${opts.target}; the round keeps no run id.`));
627
639
  }
628
- const result = await processRoundReport({
629
- gitCommonDir: await resolveGitCommonDir(),
630
- target: opts.target,
631
- round,
632
- findings: report.findings,
633
- reportSha256,
634
- cycleId: report.run?.cycle_id,
635
- ...(maxRounds !== undefined ? { maxRounds } : {}),
636
- ...(runId !== undefined ? { runId } : {}),
637
- });
638
- const classified = result.findings.map((f) => ({
639
- identity: f.identity,
640
- status: f.status,
641
- gating: findingGatingReason(f.finding),
642
- severity: f.finding.severity,
643
- file: f.finding.file,
644
- startLine: f.finding.startLine,
645
- endLine: f.finding.endLine,
646
- title: f.finding.title,
647
- ...(f.suppressReason ? { suppressReason: f.suppressReason } : {}),
648
- }));
649
- const actionable = classified.filter((f) => (f.status === 'new' || f.status === 'regating') && f.gating !== 'none');
650
- await reportConvergeEvents([
651
- buildEvent({
652
- kind: 'round_processed',
653
- convergeTarget: opts.target,
640
+ const gitCommonDir = await resolveGitCommonDir();
641
+ const target = opts.target;
642
+ const { result, classified, actionable } = await withNativeTarget(gitCommonDir, target, async (ownership) => {
643
+ const existing = await loadConvergeRunState(gitCommonDir, target);
644
+ const recoveredReport = existing?.version === 3 || report.run?.gating?.bound_classification_protocol !== undefined;
645
+ const result = await processRoundReport({
646
+ gitCommonDir,
647
+ target,
654
648
  round,
649
+ findings: recoveredReport
650
+ ? [...report.findings, ...(report.belowThresholdFindings ?? [])]
651
+ : report.findings,
652
+ ...(recoveredReport ? { evidence: { reportJson } } : {}),
653
+ reportSha256,
654
+ cycleId: report.run?.cycle_id,
655
+ ownership,
656
+ ...(maxRounds !== undefined ? { maxRounds } : {}),
655
657
  ...(runId !== undefined ? { runId } : {}),
656
- payload: {
658
+ });
659
+ const classified = result.findings.map((f) => ({
660
+ identity: f.identity,
661
+ status: f.status,
662
+ gating: f.sighting?.gating ?? findingGatingReason(f.finding),
663
+ severity: f.finding.severity,
664
+ file: f.finding.file,
665
+ startLine: f.finding.startLine,
666
+ endLine: f.finding.endLine,
667
+ title: f.finding.title,
668
+ ...(f.suppressReason ? { suppressReason: f.suppressReason } : {}),
669
+ }));
670
+ const actionable = classified.filter((f) => (f.status === 'new' || f.status === 'regating') && f.gating !== 'none');
671
+ await reportConvergeEvents([
672
+ buildEvent({
673
+ kind: 'round_processed',
674
+ convergeTarget: target,
657
675
  round,
658
- round_cap: result.roundCap,
659
- counts: result.counts,
660
- actionable_gating: actionable.length,
661
- // Which identity each sighting was matched to, so the server
662
- // can apply standing verdicts to keys that moved (IO-12601).
663
- identities: roundIdentities(result.findings),
664
- },
665
- }),
666
- ...(maxRounds !== undefined
667
- ? [buildEvent({ kind: 'cap_changed', convergeTarget: opts.target, round, payload: { kind: 'rounds', to: result.roundCap } })]
668
- : []),
669
- ]);
676
+ ...(runId !== undefined ? { runId } : {}),
677
+ payload: {
678
+ round,
679
+ round_cap: result.roundCap,
680
+ counts: result.counts,
681
+ actionable_gating: result.actionableIdentities?.length ?? actionable.length,
682
+ ...(result.classificationVersion
683
+ ? {
684
+ classification_version: result.classificationVersion,
685
+ report_json_sha256: result.reportBinding.reportSha256,
686
+ ...(result.legacyPendingIdentities
687
+ ? { legacy_pending_identities: result.legacyPendingIdentities }
688
+ : {}),
689
+ }
690
+ : {}),
691
+ // Which identity each sighting was matched to, so the server
692
+ // can apply standing verdicts to keys that moved (IO-12601).
693
+ identities: roundIdentities(result.findings),
694
+ },
695
+ }),
696
+ ...(maxRounds !== undefined
697
+ ? [buildEvent({ kind: 'cap_changed', convergeTarget: target, round,
698
+ payload: { kind: 'rounds', to: result.roundCap } })]
699
+ : []),
700
+ ]);
701
+ return { result, classified, actionable };
702
+ });
670
703
  if (opts.json) {
671
704
  console.log(JSON.stringify({
672
705
  target: opts.target,
@@ -674,8 +707,11 @@ program
674
707
  roundCap: result.roundCap,
675
708
  reviewerHealth: blockingHealthJson(health),
676
709
  counts: result.counts,
677
- actionableGating: actionable.length,
710
+ actionableGating: result.actionableIdentities?.length ?? actionable.length,
678
711
  findings: classified,
712
+ ...(result.recoveryProjection
713
+ ? { recoveryProjection: result.recoveryProjection }
714
+ : {}),
679
715
  }, null, 2));
680
716
  return;
681
717
  }
@@ -683,7 +719,7 @@ program
683
719
  console.log(`Round ${round}/${result.roundCap} for ${opts.target}: ` +
684
720
  `${result.counts.new} new, ${result.counts.repeat} repeat, ` +
685
721
  `${result.counts.suppressed} suppressed, ${result.counts.regating} regating · ` +
686
- `${actionable.length} actionable gating finding(s)`);
722
+ `${result.actionableIdentities?.length ?? actionable.length} actionable gating finding(s)`);
687
723
  for (const f of actionable) {
688
724
  console.log(` [${f.status}] ${f.identity} ${f.file}:${f.startLine} — ${f.title}`);
689
725
  }
@@ -777,68 +813,74 @@ program
777
813
  if (new Set(verdicts.map(({ key }) => key)).size !== verdicts.length) {
778
814
  throw new ConvergeRunStateError('Pass each finding identity only once, as either fixed or dismissed.');
779
815
  }
780
- const { entries: updated, resolution, runId: roundRun } = await recordVerdicts({
781
- gitCommonDir: await resolveGitCommonDir(),
782
- target: opts.target,
783
- round,
784
- verdicts,
785
- runId: opts.runId,
786
- });
787
- // Feed the cross-run precision history (RCL-27) — fail-soft, the
788
- // verdicts above are already durably recorded.
789
- try {
790
- const ts = new Date().toISOString();
791
- await appendOutcomes(updated
792
- .filter((e) => e.verdict !== undefined && e.models.length > 0)
793
- .map((e) => ({
794
- ts,
795
- verdict: e.verdict,
796
- models: e.models,
797
- severity: e.verdictSeverity ?? e.severity,
798
- target: opts.target,
799
- findingKey: e.key,
800
- source: 'live',
801
- })));
802
- }
803
- catch (err) {
804
- // Advisory history; verdict recording must not fail over it —
805
- // but say so, or a broken store silently stops learning.
806
- console.warn(`Model-stats store unavailable (outcomes not recorded): ${String(err)}`);
807
- }
808
- await reportConvergeEvents([
809
- buildEvent({
810
- kind: 'verdicts_recorded',
811
- convergeTarget: opts.target,
816
+ const target = opts.target;
817
+ const gitCommonDir = await resolveGitCommonDir();
818
+ const { entries: updated, resolution, runId: roundRun } = await withNativeTarget(gitCommonDir, target, async (ownership) => {
819
+ const recorded = await recordVerdicts({
820
+ gitCommonDir,
821
+ target,
812
822
  round,
813
- ...(roundRun !== undefined ? { runId: roundRun } : {}),
814
- payload: {
815
- verdicts: updated.map((e) => ({
816
- identity_key: e.key,
817
- verdict: e.verdict,
818
- // Verdict reasons are user-authored prose: scrubbed like every other free text that leaves the machine.
819
- ...(e.verdictReason !== undefined ? { reason: scrubText(e.verdictReason, 500) } : {}),
820
- severity: e.verdictSeverity ?? e.severity,
821
- models: e.models,
822
- })),
823
- },
824
- }),
825
- ...(resolution
826
- ? [
827
- buildEvent({
828
- kind: 'resolution',
829
- convergeTarget: opts.target,
830
- round,
831
- ...(roundRun !== undefined ? { runId: roundRun } : {}),
832
- payload: {
833
- status: resolution.status,
834
- actionable: resolution.actionable,
835
- unresolved: resolution.unresolved.length,
836
- fixed_this_round: resolution.fixedThisRound,
837
- },
838
- }),
839
- ]
840
- : []),
841
- ]);
823
+ verdicts,
824
+ runId: opts.runId,
825
+ ownership,
826
+ });
827
+ // Feed the cross-run precision history (RCL-27) — fail-soft, the
828
+ // verdicts above are already durably recorded.
829
+ try {
830
+ const ts = new Date().toISOString();
831
+ await appendOutcomes(recorded.entries
832
+ .filter((e) => e.verdict !== undefined && e.models.length > 0)
833
+ .map((e) => ({
834
+ ts,
835
+ verdict: e.verdict,
836
+ models: e.models,
837
+ severity: e.verdictSeverity ?? e.severity,
838
+ target,
839
+ findingKey: e.key,
840
+ source: 'live',
841
+ })));
842
+ }
843
+ catch (err) {
844
+ // Advisory history; verdict recording must not fail over it —
845
+ // but say so, or a broken store silently stops learning.
846
+ console.warn(`Model-stats store unavailable (outcomes not recorded): ${String(err)}`);
847
+ }
848
+ await reportConvergeEvents([
849
+ buildEvent({
850
+ kind: 'verdicts_recorded',
851
+ convergeTarget: target,
852
+ round,
853
+ ...(recorded.runId !== undefined ? { runId: recorded.runId } : {}),
854
+ payload: {
855
+ verdicts: recorded.entries.map((e) => ({
856
+ identity_key: e.key,
857
+ verdict: e.verdict,
858
+ // Verdict reasons are user-authored prose: scrubbed like every other free text that leaves the machine.
859
+ ...(e.verdictReason !== undefined ? { reason: scrubText(e.verdictReason, 500) } : {}),
860
+ severity: e.verdictSeverity ?? e.severity,
861
+ models: e.models,
862
+ })),
863
+ },
864
+ }),
865
+ ...(recorded.resolution
866
+ ? [
867
+ buildEvent({
868
+ kind: 'resolution',
869
+ convergeTarget: target,
870
+ round,
871
+ ...(recorded.runId !== undefined ? { runId: recorded.runId } : {}),
872
+ payload: {
873
+ status: recorded.resolution.status,
874
+ actionable: recorded.resolution.actionable,
875
+ unresolved: recorded.resolution.unresolved.length,
876
+ fixed_this_round: recorded.resolution.fixedThisRound,
877
+ },
878
+ }),
879
+ ]
880
+ : []),
881
+ ]);
882
+ return recorded;
883
+ });
842
884
  if (opts.json) {
843
885
  console.log(JSON.stringify({
844
886
  target: opts.target,
@@ -885,19 +927,31 @@ const telemetry = program
885
927
  .description('Evidence delivery to Harness: credentials, queued deliveries and retained rejected reports');
886
928
  telemetry
887
929
  .command('recover-reviewer')
888
- .description('Deliver one exact retained terminal reviewer report without restarting reviewers or verification')
889
- .requiredOption('--target <target>', 'Exact guarded convergence target')
890
- .requiredOption('--run <id>', 'Exact retained terminal run UUID')
930
+ .description('Recover one terminal reviewer delivery without restarting reviewers')
931
+ .option('--preview', 'Write a pinned manifest without changing the server')
932
+ .option('--apply', 'Apply one exact reviewed recovery manifest')
933
+ .option('--resume', 'Resume the same journaled recovery operation')
934
+ .option('--manifest <path>', 'Immutable recovery manifest path; required for preview, apply or resume')
935
+ .option('--manifest-sha256 <sha256>', 'Required exact manifest digest for apply or resume')
936
+ .option('--target <target>', 'Exact guarded convergence target (preview or legacy no-outbox delivery)')
937
+ .option('--run <id>', 'Exact retained terminal run UUID (preview or legacy no-outbox delivery)')
891
938
  .option('--json', 'Output JSON')
892
939
  .action(async (opts) => {
893
940
  try {
894
941
  const runtime = await createTelemetryRuntime({ rclVersion: RCL_VERSION });
895
- const result = await deliverTerminalReviewerRun(runtime, { target: opts.target, runId: opts.run });
942
+ const result = await deliverTerminalReviewerRun(runtime, { preview: opts.preview, apply: opts.apply, resume: opts.resume,
943
+ manifest: opts.manifest, manifestSha256: opts.manifestSha256, target: opts.target, runId: opts.run });
896
944
  if (opts.json)
897
945
  console.log(JSON.stringify(result, null, 2));
946
+ else if (!('status' in result)) {
947
+ const legacy = result;
948
+ console.log(`${legacy.outcome.line}\nReport ${legacy.reportSha256}; reviewer artifact ${legacy.reviewerArtifactSha256}`);
949
+ }
950
+ else if (result.status === 'prepared')
951
+ console.log(`Recovery manifest ${result.manifest}\nSHA-256 ${result.manifest_sha256}\nOperation ${result.operation_id}`);
898
952
  else
899
- console.log(`${result.outcome.line}\nReport ${result.reportSha256}; reviewer artifact ${result.reviewerArtifactSha256}`);
900
- process.exitCode = result.outcome.exitCode;
953
+ console.log(`Reviewer recovery complete for ${result.run_id}\nJournal ${result.journal}\nAcknowledgement ${result.recovery_acknowledgement}`);
954
+ process.exitCode = !('status' in result) ? result.outcome.exitCode : 0;
901
955
  }
902
956
  catch (error) {
903
957
  const message = scrubText(error instanceof Error ? error.message : String(error), 300);
@@ -1053,6 +1107,22 @@ evidenceCmd
1053
1107
  .action(async (runId, opts) => {
1054
1108
  process.exitCode = await runEvidenceShow(runId ?? '', opts, evidenceDeps());
1055
1109
  });
1110
+ evidenceCmd
1111
+ .command('recover-claim')
1112
+ .description('Preview, apply or resume an evidence-backed claim correction on the same target')
1113
+ .option('--preview', 'Read immutable evidence and write an exclusive preview manifest')
1114
+ .option('--apply', 'Apply the explicitly pinned manifest')
1115
+ .option('--resume', 'Resume the same operation after independently reading accepted receipts')
1116
+ .option('--selection <path>', 'Explicit source and claim selection for preview')
1117
+ .option('--adopt-manifest <path>', 'Existing operation manifest to adopt during preview')
1118
+ .option('--adopt-manifest-sha256 <sha256>', 'Exact digest of the existing operation manifest')
1119
+ .requiredOption('--manifest <path>', 'Exclusive preview manifest or existing operation manifest')
1120
+ .option('--manifest-sha256 <sha256>', 'Exact reviewed manifest digest for apply or resume')
1121
+ .option('--json', 'Print machine-readable operation status')
1122
+ .action(async (opts) => {
1123
+ const { runPublicClaimRecovery } = await import('./evidence/recover-claim.js');
1124
+ process.exitCode = await runPublicClaimRecovery(opts, evidenceDeps());
1125
+ });
1056
1126
  evidenceCmd
1057
1127
  .command('recover-run')
1058
1128
  .description('Preview, apply or resume delivery of one immutable original asserted run; no review, events or native accounting')
@@ -1359,7 +1429,20 @@ async function prepareCouncil(spinner, opts, fallbackRoles, attestation) {
1359
1429
  const startedAt = new Date();
1360
1430
  // Validate the converge context first: a bad --round must fail before any
1361
1431
  // model time is spent, not after the council has run.
1362
- const converge = resolveConvergeContext({ convergeTarget: opts.convergeTarget, round: opts.round, attempt: opts.attempt }, reviewConvergeEnvironment(opts));
1432
+ let converge = resolveConvergeContext({ convergeTarget: opts.convergeTarget, round: opts.round, attempt: opts.attempt }, reviewConvergeEnvironment(opts));
1433
+ if (!opts.startOver && opts.guardedConverge && converge && converge.round === undefined) {
1434
+ const state = await loadConvergeRunState(await resolveGitCommonDir(), converge.target);
1435
+ converge = { ...converge, round: Math.max(0, ...(state?.rounds.map(entry => entry.round) ?? [])) + 1 };
1436
+ }
1437
+ const recoveredProduction = converge && !opts.startOver
1438
+ ? await selectCurrentRecoveredProduction(converge)
1439
+ : undefined;
1440
+ if (recoveredProduction && !opts.guardedConverge) {
1441
+ throw new ReviewLaunchRefused('recovery_guard_required', 'Recovered-v3 production requires --guarded-converge so the provider call and report admission remain bound to one durable attempt.');
1442
+ }
1443
+ if (opts.guardedConverge && converge?.attempt !== undefined && !recoveredProduction) {
1444
+ throw new ReviewLaunchRefused('preclaimed_attempt_ineligible', 'An explicit guarded attempt may only hand off the exact standalone claim for recovered-v3 production.');
1445
+ }
1363
1446
  if (!opts.exportPendingPackage)
1364
1447
  await fetchHarnessKeys(spinner, attestation?.credential);
1365
1448
  const config = await loadConfig(opts.config, undefined, { preserveDefaultRoster: opts.guardedConverge });
@@ -1553,7 +1636,10 @@ async function prepareCouncil(spinner, opts, fallbackRoles, attestation) {
1553
1636
  ...(pendingClaimPlan ? { pendingClaimPlan } : {}),
1554
1637
  explicit: explicitReviewers !== undefined,
1555
1638
  coreModels: models,
1556
- ...(converge ? { converge } : {}),
1639
+ ...(converge ? { converge: { ...converge, ...(recoveredProduction ? { recovery_source: {
1640
+ version: 1, native_sha256: recoveredProduction.nativeSha256,
1641
+ } } : {}) } } : {}),
1642
+ ...(recoveredProduction ? { recoveredProduction } : {}),
1557
1643
  startedAt,
1558
1644
  };
1559
1645
  }
@@ -1584,8 +1670,10 @@ async function discoverCycleReview(target, opts) {
1584
1670
  const active = opts.startOver ? undefined : await loadConvergeRunState(common, targetKey);
1585
1671
  if (!opts.startOver && !active?.cycle)
1586
1672
  return opts;
1587
- if (opts.attempt !== undefined || (opts.startOver && opts.round !== undefined) || opts.attest)
1673
+ const recoveredPreclaim = active?.version === 3 && opts.guardedConverge && opts.attempt !== undefined;
1674
+ if ((opts.attempt !== undefined && !recoveredPreclaim) || (opts.startOver && opts.round !== undefined) || opts.attest) {
1588
1675
  throw new Error('A fresh review assigns its own ordinals and needs an ordinary actor credential');
1676
+ }
1589
1677
  if (opts.telemetry === false)
1590
1678
  throw new Error('Fresh review cycles require Harness evidence');
1591
1679
  const paths = opts.jsonFile ? {} : await freshReviewOutputPaths(common);
@@ -1680,8 +1768,8 @@ async function runReview(target, opts) {
1680
1768
  const converge = resolveConvergeContext(opts, reviewConvergeEnvironment(opts));
1681
1769
  if (!converge)
1682
1770
  throw new ReviewLaunchRefused('target_required', 'A guarded launch requires --converge-target.');
1683
- if (converge.attempt !== undefined || opts.attest) {
1684
- throw new ReviewLaunchRefused('incompatible_launch', 'Guarded review claims its own attempt; do not preclaim, pass --attempt, or combine it with --attest.');
1771
+ if (opts.attest) {
1772
+ throw new ReviewLaunchRefused('incompatible_launch', 'Guarded review cannot be combined with --attest.');
1685
1773
  }
1686
1774
  }
1687
1775
  // Exactly one review source: a positional target, --staged, or --working-tree
@@ -1906,6 +1994,8 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
1906
1994
  throw new ReviewLaunchRefused('pending_async_binding_invalid', 'Retained async bindings must be comma-separated lowercase SHA-256 digests, or "none" for an empty cycle history.');
1907
1995
  }
1908
1996
  const migrationPackage = opts.ordinaryPendingPackage === undefined ? undefined : JSON.parse((await readStable(opts.ordinaryPendingPackage)).text);
1997
+ const migrationGuardedInput = migrationPackage
1998
+ ? ordinaryPendingGuardedInput(migrationPackage) : undefined;
1909
1999
  if (opts.expectPrHeadSha !== undefined && migrationPackage?.version !== 2) {
1910
2000
  throw new ReviewLaunchRefused('pending_finalize_incompatible', '--expect-pr-head-sha requires a cycle-backed version-2 pending package.');
1911
2001
  }
@@ -1955,7 +2045,7 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
1955
2045
  return [...found.values()].sort((a, b) => a.sha256.localeCompare(b.sha256));
1956
2046
  };
1957
2047
  if (opts.previewPending && migrationPackage) {
1958
- const inputSha256 = guardedInputSha256(migrationPackage.guardedInput);
2048
+ const inputSha256 = guardedInputSha256(migrationGuardedInput);
1959
2049
  const preview = await previewOrdinaryPendingLaunch({ gitCommonDir: common,
1960
2050
  target: prepared.converge.target, headSha: extra.target.headSha ?? '', baseSha: extra.target.baseSha ?? '',
1961
2051
  pendingInputSha256: inputSha256, recoveryInputSha256: inputSha256,
@@ -1974,7 +2064,7 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
1974
2064
  return undefined;
1975
2065
  }
1976
2066
  if (opts.finalizePendingOnly && migrationPackage) {
1977
- const inputSha256 = guardedInputSha256(migrationPackage.guardedInput);
2067
+ const inputSha256 = guardedInputSha256(migrationGuardedInput);
1978
2068
  const result = await finalizeOrdinaryPendingLaunch({ gitCommonDir: common,
1979
2069
  target: prepared.converge.target, headSha: extra.target.headSha ?? '',
1980
2070
  baseSha: extra.target.baseSha ?? '', pendingInputSha256: inputSha256,
@@ -2017,7 +2107,7 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
2017
2107
  .slice(0, MAX_ASYNC_CALLS_PER_ROUND);
2018
2108
  const pendingClaimAsyncPrompts = await Promise.all(pendingClaimAsyncChunks.map(({ assignment, chunk }) => buildPrompt(chunk, assignment.role, { contextDocs: pendingClaimContext, plan: planContext })));
2019
2109
  const claimedInputSha256 = migrationPackage
2020
- ? guardedInputSha256(migrationPackage.guardedInput)
2110
+ ? guardedInputSha256(migrationGuardedInput)
2021
2111
  : guardedInputSha256({
2022
2112
  head: extra.target.headSha, kind: extra.target.kind, repo: extra.target.repo, pr: extra.target.prNumber,
2023
2113
  diff: diffDigest(diff.files), config: configDigest(config), roster: pendingClaimRoster,
@@ -2119,6 +2209,7 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
2119
2209
  target: prepared.converge.target,
2120
2210
  startOver: opts.startOver, cycleRemote,
2121
2211
  boundFixRecovery,
2212
+ recoverySource: prepared.converge.recovery_source,
2122
2213
  headSha: extra.target.headSha ?? '',
2123
2214
  inputSha256: guardedInputSha256(guardedInput),
2124
2215
  round: prepared.converge.round,
@@ -2127,6 +2218,7 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
2127
2218
  ...(opts.retryReport ? { legacyRetry: { reportPath: opts.retryReport, config, roster,
2128
2219
  historicalPlan: prepared.historicalRetryPlan === true } } : {}),
2129
2220
  maxAttempts: opts.maxAttempts === undefined ? undefined : Number(opts.maxAttempts),
2221
+ consumeAttempt: prepared.recoveredProduction ? prepared.converge.attempt : undefined,
2130
2222
  maxRounds: opts.maxRounds === undefined ? undefined : Number(opts.maxRounds),
2131
2223
  validate: async () => {
2132
2224
  validateLaunchProviders(roster.map(entry => entry.provider));
@@ -2144,7 +2236,7 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
2144
2236
  baseSha: retained.baseSha } };
2145
2237
  },
2146
2238
  onClaim: async (claim) => {
2147
- if (opts.telemetry !== false)
2239
+ if (!claim.preclaimed && opts.telemetry !== false)
2148
2240
  await reportConvergeEvents([buildEvent({
2149
2241
  kind: 'attempt_claimed', convergeTarget: claim.target, attempt: claim.attempt,
2150
2242
  payload: { attempt: claim.attempt, cap: claim.cap, ...(claim.cycle ? { cycle_id: claim.cycle.id } : {}),
@@ -2441,7 +2533,10 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
2441
2533
  startedAt: extra.pendingResume
2442
2534
  ? new Date(extra.pendingResume.launch.startedAtMs)
2443
2535
  : prepared.startedAt,
2444
- ...(prepared.converge ? { converge: { target: prepared.converge.target, round: prepared.converge.round, attempt: prepared.converge.attempt }, cycleId: prepared.converge.cycleId } : {}),
2536
+ ...(prepared.converge ? { converge: {
2537
+ target: prepared.converge.target, round: prepared.converge.round, attempt: prepared.converge.attempt,
2538
+ ...(prepared.converge.recovery_source ? { recovery_source: prepared.converge.recovery_source } : {}),
2539
+ }, cycleId: prepared.converge.cycleId } : {}),
2445
2540
  };
2446
2541
  const assemblyDependencies = {
2447
2542
  onStage: postReviewStage,
@@ -2498,7 +2593,7 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
2498
2593
  else
2499
2594
  result = await assembleCompletedReview({
2500
2595
  chunkReviews, arrivedAsync, asyncLaunched, startTime, roleMap, config, diff,
2501
- gatingConfig: prepared.gatingConfig, modelWeights, run: runInput,
2596
+ gatingConfig: prepared.gatingConfig, recoveredProduction: prepared.recoveredProduction, modelWeights, run: runInput,
2502
2597
  }, assemblyDependencies);
2503
2598
  const { run } = result;
2504
2599
  // The same derivation converge-report and the server apply to this report.