@nexus-cortex/core 4.120.0 → 4.122.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 (34) hide show
  1. package/.env.defaults +32 -0
  2. package/dist/config/RuntimeConfigRegistry.d.ts.map +1 -1
  3. package/dist/config/RuntimeConfigRegistry.js +7 -0
  4. package/dist/config/RuntimeConfigRegistry.js.map +1 -1
  5. package/dist/config/SettingsLoader.d.ts.map +1 -1
  6. package/dist/config/SettingsLoader.js +7 -0
  7. package/dist/config/SettingsLoader.js.map +1 -1
  8. package/dist/config/SettingsSchema.d.ts +7 -0
  9. package/dist/config/SettingsSchema.d.ts.map +1 -1
  10. package/dist/config/SettingsSchema.js +31 -0
  11. package/dist/config/SettingsSchema.js.map +1 -1
  12. package/dist/config/effectiveConfig.d.ts.map +1 -1
  13. package/dist/config/effectiveConfig.js +7 -0
  14. package/dist/config/effectiveConfig.js.map +1 -1
  15. package/dist/middleware/HelperModelMiddleware.d.ts +13 -0
  16. package/dist/middleware/HelperModelMiddleware.d.ts.map +1 -1
  17. package/dist/middleware/HelperModelMiddleware.js +18 -0
  18. package/dist/middleware/HelperModelMiddleware.js.map +1 -1
  19. package/dist/orchestrator/CortexOrchestrator.d.ts +11 -0
  20. package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
  21. package/dist/orchestrator/CortexOrchestrator.js +159 -37
  22. package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
  23. package/dist/training/DecisionStore.d.ts +1 -1
  24. package/dist/training/DecisionStore.d.ts.map +1 -1
  25. package/dist/training/DecisionStore.js.map +1 -1
  26. package/dist/training/endTurnResolver.d.ts +39 -0
  27. package/dist/training/endTurnResolver.d.ts.map +1 -1
  28. package/dist/training/endTurnResolver.js +54 -1
  29. package/dist/training/endTurnResolver.js.map +1 -1
  30. package/dist/training/independentDerivation.d.ts +62 -0
  31. package/dist/training/independentDerivation.d.ts.map +1 -0
  32. package/dist/training/independentDerivation.js +168 -0
  33. package/dist/training/independentDerivation.js.map +1 -0
  34. package/package.json +3 -3
@@ -19,8 +19,9 @@ import { join as pathJoin } from 'path';
19
19
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
20
20
  import { execSync } from 'node:child_process';
21
21
  import { ENV_RECON_COMMAND, resolveLiftPlanConfig, parsePlannerResponse } from '../training/liftPlanner.js';
22
- import { resolveEndTurnResolverConfig, parseResolverVerdict, effectiveMaxRejects, budgetedVetoEscalation, decideVetoAction, shouldFinishConfirm, buildFinishConfirmMessage, buildMeetsConfirmMessage, gapHoldable, parseSpecChecks } from '../training/endTurnResolver.js';
22
+ import { resolveEndTurnResolverConfig, parseResolverVerdict, effectiveMaxRejects, budgetedVetoEscalation, decideVetoAction, shouldFinishConfirm, buildFinishConfirmMessage, buildMeetsConfirmMessage, gapHoldable, parseSpecChecks, applyVetoFloor, holdProgressed, specCheckEvidence } from '../training/endTurnResolver.js';
23
23
  import { jevAvailable, jevNoul, buildGapHoldState, GAP_HOLD_QUESTIONS } from '../training/jevGate.js'; // R173b
24
+ import { isValueShapedTask, parseDerivationReply, methodsDiffer, parseValueLines, extractNumbers, reconcile, buildDerivationHoldMessage, isDerivationCommandAllowed } from '../training/independentDerivation.js'; // R176
24
25
  import { resolveDeadlineExitConfig, deadlineExitCallBudget, parseDeadlineExitVerdict } from '../training/deadlineExitMentor.js';
25
26
  import { isTaskShaped } from './requirementsVerification.js';
26
27
  import { extractServerSideMetadata, XAIServerSideTools, OpenAIServerSideTools, toCanonicalTool } from '../tools/ServerSideTools.js';
@@ -244,6 +245,10 @@ export class CortexOrchestrator {
244
245
  judgeAdjudicatedThisFinish = false;
245
246
  specChecks = null; // R174: blind spec-derived checks authored for this turn (null = not yet)
246
247
  specChecksMeta = { genLatencyMs: 0, refused: 0, raw: 0 }; // R174
248
+ specChecksPromise = null; // R174b: authored at lift in the background (CORTEX_JUDGE_SPEC_TESTS_AT=lift)
249
+ specFailHistory = new Map(); // R174b: consecutive identical spec failures
250
+ lastHoldMs = 0; // R173c: wall clock of the last hold/veto this turn
251
+ derivationHolds = 0; // R176: independent-derivation holds this turn (max 1)
247
252
  liftPlanText = ''; // 4.107.0: the PLAN OF ATTACK delivered at lift — handed to the resolver / deadline-exit / loop-exit judges as an advisory anchor
248
253
  effectiveDeferredLoading = true; // per-turn resolved deferred-loading (card > env > settings); set at assembly
249
254
  /**
@@ -713,6 +718,59 @@ export class CortexOrchestrator {
713
718
  }
714
719
  }
715
720
  }
721
+ /** R176: read the artifact the task names (the agent's deliverable) for reconciliation — direct, bounded, text only;
722
+ * an absolute path outside the workspace is still the task's own file, so it is read rather than refused. */
723
+ readDeliverable(cwd, artifact, maxChars = 1500) {
724
+ try {
725
+ const abs = artifact.startsWith('/') ? artifact : `${cwd}/${artifact}`;
726
+ if (!existsSync(abs))
727
+ return `${artifact}: NOT FOUND`;
728
+ const text = readFileSync(abs, 'utf8');
729
+ return `${artifact}:\n${text.length > maxChars ? text.slice(0, maxChars) + '\n…' : text}`;
730
+ }
731
+ catch (e) {
732
+ return `${artifact}: unreadable (${String(e?.message ?? e).slice(0, 80)})`;
733
+ }
734
+ }
735
+ /** R174/R174b: author the blind spec checks (task text + env report ONLY). Sets specChecks/specChecksMeta; never throws. */
736
+ async authorSpecChecks(task) {
737
+ const cfg = resolveEndTurnResolverConfig();
738
+ const store = this.getDecisionStore();
739
+ const sessionId = this.currentSessionId ?? 'unknown';
740
+ const g0 = Date.now();
741
+ if (!this.helperMiddleware?.deriveSpecChecks) {
742
+ this.specChecks = [];
743
+ return [];
744
+ }
745
+ const specTimeoutMs = mentorSurfaceTimeoutMs('endturn-resolver', parseInt(process.env.CORTEX_ENDTURN_RESOLVER_TIMEOUT_MS ?? '90000', 10));
746
+ try {
747
+ const raw = await withTimeout(this.helperMiddleware.deriveSpecChecks({ task, envReport: this.gatherEnvReport({ fresh: false }), max: cfg.specTestsMax, helperModelId: this.config.reactiveMentorship?.helperModelId }), specTimeoutMs);
748
+ const parsed = parseSpecChecks(raw ?? '', cfg.specTestsMax);
749
+ const allowed = parsed.filter((c) => isInvestigateCommandAllowed(c));
750
+ this.specChecks = allowed;
751
+ this.specChecksMeta = { genLatencyMs: Date.now() - g0, refused: parsed.length - allowed.length, raw: (raw ?? '').length };
752
+ }
753
+ catch {
754
+ this.specChecks = [];
755
+ this.specChecksMeta = { genLatencyMs: Date.now() - g0, refused: 0, raw: 0 };
756
+ }
757
+ if (store)
758
+ void store.recordEvent({ sessionId, kind: 'spec_tests', toolName: 'EndTurn', detail: { checks: this.specChecks, ...this.specChecksMeta, at: cfg.specTestsAt, mentor: this.mentorWire('endturn-resolver', cfg.effort, 1200) } }).catch(() => { });
759
+ console.warn(`[EndTurnResolver] R174 SPEC-TESTS — ${this.specChecks.length} blind check(s) authored at ${cfg.specTestsAt} in ${this.specChecksMeta.genLatencyMs} ms (${this.specChecksMeta.refused} refused)`);
760
+ return this.specChecks;
761
+ }
762
+ /** R174b: at lift, start authoring the spec checks in the background so a late first finish still has them. */
763
+ maybeAuthorSpecChecksAtLift() {
764
+ const cfg = resolveEndTurnResolverConfig();
765
+ if (!cfg.specTests || cfg.specTestsAt !== 'lift' || this.specChecks !== null || this.specChecksPromise)
766
+ return;
767
+ if (!resolveEndTurnResolver(process.env))
768
+ return;
769
+ const task = this.lastRealUserText();
770
+ if (!task.trim())
771
+ return;
772
+ this.specChecksPromise = this.authorSpecChecks(task).catch(() => []);
773
+ }
716
774
  async adjudicateEndTurn(ev, toolResults, toolUses, model) {
717
775
  if (!resolveEndTurnResolver(process.env, model.endTurnResolver))
718
776
  return;
@@ -788,39 +846,27 @@ export class CortexOrchestrator {
788
846
  let specPassed = 0;
789
847
  let specFailed = 0;
790
848
  let specInconclusive = 0;
849
+ let specSuspect = 0;
791
850
  let specResults = '';
792
- if (cfg.specTests && this.helperMiddleware.deriveSpecChecks) {
793
- if (this.specChecks === null) {
794
- const g0 = Date.now();
795
- const specTimeoutMs = mentorSurfaceTimeoutMs('endturn-resolver', parseInt(process.env.CORTEX_ENDTURN_RESOLVER_TIMEOUT_MS ?? '90000', 10));
796
- try {
797
- const raw = await withTimeout(this.helperMiddleware.deriveSpecChecks({ task, envReport: this.gatherEnvReport({ fresh: false }), max: cfg.specTestsMax, helperModelId: this.config.reactiveMentorship?.helperModelId }), specTimeoutMs);
798
- const parsed = parseSpecChecks(raw ?? '', cfg.specTestsMax);
799
- const allowed = parsed.filter((c) => isInvestigateCommandAllowed(c));
800
- this.specChecks = allowed;
801
- this.specChecksMeta = { genLatencyMs: Date.now() - g0, refused: parsed.length - allowed.length, raw: (raw ?? '').length };
802
- }
803
- catch {
804
- this.specChecks = [];
805
- this.specChecksMeta = { genLatencyMs: Date.now() - g0, refused: 0, raw: 0 };
806
- }
807
- if (store)
808
- void store.recordEvent({ sessionId, kind: 'spec_tests', toolName: 'EndTurn', detail: { checks: this.specChecks, ...this.specChecksMeta, mentor: this.mentorWire('endturn-resolver', cfg.effort, 1200) } }).catch(() => { });
809
- console.warn(`[EndTurnResolver] R174 SPEC-TESTS — ${this.specChecks.length} blind check(s) authored in ${this.specChecksMeta.genLatencyMs} ms (${this.specChecksMeta.refused} refused)`);
810
- }
811
- for (const c of this.specChecks) {
812
- const r = runCheck(judgeCwd, c, jcfg);
851
+ if (cfg.specTests) {
852
+ if (this.specChecks === null)
853
+ this.specChecks = await (this.specChecksPromise ?? this.authorSpecChecks(task)); // R174b: lift-authored or now
854
+ // R174b: two passes — classify every check, then apply repeat suppression (a check failing identically at
855
+ // specRepeatMax consecutive adjudications, while no OTHER check failed, is `suspect`: shown, not evidence).
856
+ const runs = this.specChecks.map((c) => { const r = runCheck(judgeCwd, c, jcfg); return { c, r, k: classifyCheckRun(r) }; });
857
+ const failedCmds = runs.filter((x) => x.k === 'failed').map((x) => x.c);
858
+ for (const x of runs) {
859
+ const ev = specCheckEvidence(this.specFailHistory, x.c, x.r, x.k, cfg.specRepeatMax, failedCmds.some((f) => f !== x.c));
813
860
  specRan += 1;
814
- {
815
- const k = classifyCheckRun(r);
816
- if (k === 'passed')
817
- specPassed += 1;
818
- else if (k === 'failed')
819
- specFailed += 1;
820
- else
821
- specInconclusive += 1;
822
- }
823
- specResults += (specResults ? '\n\n' : '') + r;
861
+ if (ev === 'passed')
862
+ specPassed += 1;
863
+ else if (ev === 'failed')
864
+ specFailed += 1;
865
+ else if (ev === 'suspect')
866
+ specSuspect += 1;
867
+ else
868
+ specInconclusive += 1;
869
+ specResults += (specResults ? '\n\n' : '') + x.r + (ev === 'suspect' ? '\n(this check has failed identically at consecutive finishes while everything else passed — treat it as SUSPECT, not as proof of a gap)' : '');
824
870
  }
825
871
  }
826
872
  const combinedCheck = [checkResult, namedResults ? `JUDGE-NAMED CHECKS (from your previous fix plan; executed by the harness just now):\n${namedResults}` : '',
@@ -951,8 +997,12 @@ export class CortexOrchestrator {
951
997
  }
952
998
  const holdable = gapHoldable({ gapHold: cfg.gapHold, remainingFrac: resolverRemainingFrac, minRemaining: resolveBudgetContinueMinRemaining(), planChars: verdict.plan.length, jevMode: cfg.gapHoldJev, jevFixable, jevMin: cfg.gapHoldJevMin });
953
999
  const checksFailed = namedFailed > 0 || specFailed > 0; // R166 + R174
1000
+ // R173c: a re-hold needs real work since the last hold (elapsed time or a changed open-items list), not just tool calls.
1001
+ const msSinceLastHold = this.lastHoldMs > 0 ? Date.now() - this.lastHoldMs : null;
1002
+ const holdProg = holdProgressed({ rejects: this.endTurnResolverRejects, msSinceLastHold, priorPlan: this.judgePriorPlan, plan: verdict.plan, minIntervalMs: cfg.gapHoldMinIntervalMs, maxSimilarity: cfg.gapHoldPlanMaxSimilarity });
1003
+ const progressedForHold = holdable ? (progressed && holdProg) : progressed;
954
1004
  let action = cfg.semantic
955
- ? decideVetoAction({ meets: verdict.meets, blank: verdict.blank, retire: verdict.retire && cfg.abstain, confidence: verdict.confidence, rejects: this.endTurnResolverRejects, cap: resolverCap, progressed, escalated: this.judgeEscalated, checksFailed, vetoMode: cfg.vetoMode, evidenceCap: cfg.evidenceCap, gapHoldable: holdable })
1005
+ ? decideVetoAction({ meets: verdict.meets, blank: verdict.blank, retire: verdict.retire && cfg.abstain, confidence: verdict.confidence, rejects: this.endTurnResolverRejects, cap: resolverCap, progressed: progressedForHold, escalated: this.judgeEscalated, checksFailed, vetoMode: cfg.vetoMode, evidenceCap: cfg.evidenceCap, gapHoldable: holdable })
956
1006
  : (verdict.blank || (verdict.retire && cfg.abstain) || verdict.meets || !verdict.plan ? 'accept' : 'veto');
957
1007
  if (action === 'escalate') {
958
1008
  // R165: the junior re-attested without working the plan — one thinking-on adjudication before we accept with the gap recorded.
@@ -966,10 +1016,60 @@ export class CortexOrchestrator {
966
1016
  verdict = v2;
967
1017
  }
968
1018
  }
969
- action = decideVetoAction({ meets: verdict.meets, blank: verdict.blank, retire: verdict.retire && cfg.abstain, confidence: verdict.confidence, rejects: this.endTurnResolverRejects, cap: resolverCap, progressed, escalated: true, checksFailed, vetoMode: cfg.vetoMode, evidenceCap: cfg.evidenceCap, gapHoldable: holdable });
1019
+ action = decideVetoAction({ meets: verdict.meets, blank: verdict.blank, retire: verdict.retire && cfg.abstain, confidence: verdict.confidence, rejects: this.endTurnResolverRejects, cap: resolverCap, progressed: progressedForHold, escalated: true, checksFailed, vetoMode: cfg.vetoMode, evidenceCap: cfg.evidenceCap, gapHoldable: holdable });
970
1020
  if (action === 'escalate')
971
1021
  action = 'accept-with-gap';
972
1022
  }
1023
+ // R173c: the budget floor — below CORTEX_JUDGE_VETO_MIN_REMAINING nothing holds the finish (the gap is recorded).
1024
+ const belowFloor = cfg.vetoMinRemaining > 0 && resolverRemainingFrac !== null && resolverRemainingFrac < cfg.vetoMinRemaining && action === 'veto';
1025
+ action = applyVetoFloor(action, resolverRemainingFrac, cfg.vetoMinRemaining);
1026
+ // R176 HB-INDEPENDENT-DERIVATION: on a value-shaped task, before a finish that would otherwise STAND, recompute the result by a
1027
+ // different method and hold ONCE on disagreement. Never below the budget floor; never more than once per turn.
1028
+ let derivationHold = null;
1029
+ let derivationInfo = null;
1030
+ if (cfg.derivation === 'on' && action !== 'veto' && action !== 'escalate' && this.derivationHolds < 1 && !belowFloor &&
1031
+ !(cfg.vetoMinRemaining > 0 && resolverRemainingFrac !== null && resolverRemainingFrac < cfg.vetoMinRemaining) && this.helperMiddleware.deriveIndependentCheck) {
1032
+ const shape = isValueShapedTask(task);
1033
+ derivationInfo = { valueShaped: shape.valueShaped, reason: shape.reason, artifacts: shape.artifacts };
1034
+ if (shape.valueShaped) {
1035
+ const d0 = Date.now();
1036
+ try {
1037
+ const deliverable = shape.artifacts.map((a) => this.readDeliverable(judgeCwd, a)).join('\n\n');
1038
+ const raw = await withTimeout(this.helperMiddleware.deriveIndependentCheck({ task, deliverable: `${deliverable}\n\nAGENT FINAL MESSAGE:\n${this.lastAssistantText().slice(0, 2000)}`, agentSummary: attestation, envReport: this.gatherEnvReport({ fresh: false }), values: [], helperModelId: this.config.reactiveMentorship?.helperModelId }), timeoutMs);
1039
+ const plan = parseDerivationReply(raw ?? '', 3);
1040
+ const allowed = plan.checks.filter((c) => isDerivationCommandAllowed(c, isInvestigateCommandAllowed));
1041
+ const refusedCmds = plan.checks.filter((c) => !allowed.includes(c)).map((c) => c.slice(0, 200));
1042
+ const derived = {};
1043
+ const outputs = [];
1044
+ for (const c of allowed) {
1045
+ const r = runCheck(judgeCwd, c, jcfg);
1046
+ outputs.push(r);
1047
+ Object.assign(derived, parseValueLines(r));
1048
+ }
1049
+ // Fallback when the author printed values without the `VALUE name=` contract: take the LAST few numbers each check printed
1050
+ // and require that at least one matches the deliverable — conservative (a disagreement needs every printed number to miss).
1051
+ let fallback = false;
1052
+ if (!Object.keys(derived).length && outputs.length) {
1053
+ const tail = outputs.flatMap((o) => extractNumbers(o.split('\n').slice(1).join('\n')).slice(-3));
1054
+ tail.slice(-6).forEach((v, i) => { derived[`printed${i + 1}`] = String(v); });
1055
+ fallback = tail.length > 0;
1056
+ }
1057
+ let rec = reconcile(`${deliverable}\n${this.lastAssistantText()}`, derived, cfg.derivationTol);
1058
+ if (fallback && rec.compared.some((c) => c.matched !== null))
1059
+ rec = { ...rec, agreement: 'agree' };
1060
+ const differ = methodsDiffer(plan.methodAgent, plan.methodIndependent);
1061
+ derivationInfo = { ...derivationInfo, methodAgent: plan.methodAgent, methodIndependent: plan.methodIndependent, methodsDiffer: differ, checks: allowed.length, refused: refusedCmds.length, refusedCmds, fallback, checkOutputs: outputs.map((o) => o.slice(0, 600)), derived, agreement: rec.agreement, compared: rec.compared, latencyMs: Date.now() - d0 };
1062
+ if (rec.agreement === 'disagree' && differ)
1063
+ derivationHold = { message: buildDerivationHoldMessage({ methodIndependent: plan.methodIndependent, compared: rec.compared, remainingFrac: resolverRemainingFrac }) };
1064
+ console.warn(`[EndTurnResolver] R176 DERIVATION — ${rec.agreement} (${allowed.length} check(s), methodsDiffer=${differ}) in ${Date.now() - d0} ms`);
1065
+ }
1066
+ catch (e) {
1067
+ derivationInfo = { ...derivationInfo, error: String(e?.message ?? e).slice(0, 120) };
1068
+ }
1069
+ }
1070
+ if (store)
1071
+ void store.recordEvent({ sessionId, kind: 'independent_derivation', toolName: 'EndTurn', detail: { ...derivationInfo, held: !!derivationHold, remainingFrac: resolverRemainingFrac } }).catch(() => { });
1072
+ }
973
1073
  this.judgeAdjudicatedThisFinish = verdict.parsed;
974
1074
  const latencyMs = Date.now() - t0;
975
1075
  // OBSERVABILITY (resolver-AB follow-up, 2026-09-05): bank the TEXT the resolver produced +
@@ -990,7 +1090,9 @@ export class CortexOrchestrator {
990
1090
  vetoMode: cfg.vetoMode, evidenceCap: cfg.evidenceCap, namedRanNow, namedInconclusive, // R166 / R166b
991
1091
  toolRounds: cfg.toolRounds, roundsUsed, investigateChecks, investigateReads, investigateRefused, autoLooped, toolAutoLoop: cfg.toolAutoLoop, roundLatencyMs, evidenceChars: evidenceRounds.join('').length, // R170 / R170b
992
1092
  gapHold: cfg.gapHold, gapHoldable: holdable, jevMode: cfg.gapHoldJev, jevFixable, jevLatencyMs, // R173 / R173b
993
- specTests: cfg.specTests, specChecks: this.specChecks?.length ?? 0, specRan, specPassed, specFailed, specInconclusive, specGenLatencyMs: this.specChecksMeta.genLatencyMs, // R174
1093
+ specTests: cfg.specTests, specChecks: this.specChecks?.length ?? 0, specRan, specPassed, specFailed, specInconclusive, specSuspect, specGenLatencyMs: this.specChecksMeta.genLatencyMs, specTestsAt: cfg.specTestsAt, // R174 / R174b
1094
+ vetoMinRemaining: cfg.vetoMinRemaining, belowFloor, holdProgressed: holdProg, msSinceLastHold, // R173c
1095
+ derivation: cfg.derivation, derivationAgreement: derivationInfo?.agreement ?? null, derivationHeld: !!derivationHold, // R176
994
1096
  latencyMs, rawLen: (text ?? '').length,
995
1097
  deltaChars: workspaceDelta.length, checkRan: !!checkResult, checkPassed: checkResult ? /→ PASSED/.test(checkResult) : null,
996
1098
  liftPlanChars: this.liftPlanText.length,
@@ -1016,6 +1118,7 @@ export class CortexOrchestrator {
1016
1118
  }
1017
1119
  else if (action === 'veto') {
1018
1120
  this.endTurnResolverRejects += 1;
1121
+ this.lastHoldMs = Date.now(); // R173c
1019
1122
  ev.endTurnCalled = false; // VETO the finish
1020
1123
  if (cfg.semantic) {
1021
1124
  this.judgeNamedChecks = verdict.checks;
@@ -1033,6 +1136,15 @@ export class CortexOrchestrator {
1033
1136
  budgetedVetoEscalation(this.endTurnResolverRejects, resolverCap, resolverRemainingMs ?? 0, this.turnDeadlineMsActive); // R160
1034
1137
  console.warn(`[EndTurnResolver] GAP — vetoed finish (${verdict.plan.length}-char plan, reject ${this.endTurnResolverRejects}/${resolverCap}${resolverRemainingFrac === null ? '' : `, budget remaining ${Math.round(resolverRemainingFrac * 100)}%`})`);
1035
1138
  }
1139
+ else if (derivationHold) {
1140
+ // R176: the finish would stand, but an independent recomputation disagrees — hold once with both values shown.
1141
+ this.derivationHolds += 1;
1142
+ this.lastHoldMs = Date.now();
1143
+ ev.endTurnCalled = false;
1144
+ et.is_error = true;
1145
+ et.content = derivationHold.message;
1146
+ console.warn(`[EndTurnResolver] R176 DERIVATION-HOLD — finish held once on a disagreeing independent recomputation`);
1147
+ }
1036
1148
  else if ((action === 'accept-with-gap' || action === 'accept-low-confidence') &&
1037
1149
  shouldFinishConfirm({ action, remainingFrac: resolverRemainingFrac, confirmsUsed: this.finishConfirms, cfg })) {
1038
1150
  // R167 HB-FINISH-CONFIRM: the judge would accept with a gap and budget remains — hold ONCE with an informed
@@ -2010,7 +2122,11 @@ export class CortexOrchestrator {
2010
2122
  this.judgeEscalated = false;
2011
2123
  this.judgeAdjudicatedThisFinish = false;
2012
2124
  this.specChecks = null;
2013
- this.specChecksMeta = { genLatencyMs: 0, refused: 0, raw: 0 }; // R165 per-turn / R174
2125
+ this.specChecksMeta = { genLatencyMs: 0, refused: 0, raw: 0 };
2126
+ this.specChecksPromise = null;
2127
+ this.specFailHistory.clear();
2128
+ this.lastHoldMs = 0;
2129
+ this.derivationHolds = 0; // R165 per-turn / R174 / R173c / R176
2014
2130
  this.recordDsmlRecovery(currentAssistantCanonicalMessage);
2015
2131
  let toolCallIteration = 0;
2016
2132
  // R137 HB-POLL-REPEAT-BREAKER: the exact-repeat breaker used to force-exit by overwriting
@@ -3226,6 +3342,7 @@ export class CortexOrchestrator {
3226
3342
  await this.deliverDeferredCorpusAtLift(effectiveModel);
3227
3343
  this.deliverLiftNudge(allTools, effectiveModel); // A′ proposal-1: SearchTools/AskForAdvice signpost at lift
3228
3344
  await this.deliverLiftPlanAtLift(effectiveModel); // LIFT_MENTOR_PLANNER: bounded mentor-planner at lift (dark unless CORTEX_LIFT_PLAN)
3345
+ this.maybeAuthorSpecChecksAtLift(); // R174b: blind spec checks authored in the background at lift (CORTEX_JUDGE_SPEC_TESTS_AT=lift)
3229
3346
  if (this.config.debug)
3230
3347
  console.log('[Anchor] lifted at first tool_result boundary — session profile applies');
3231
3348
  }
@@ -4215,7 +4332,11 @@ export class CortexOrchestrator {
4215
4332
  this.judgeEscalated = false;
4216
4333
  this.judgeAdjudicatedThisFinish = false;
4217
4334
  this.specChecks = null;
4218
- this.specChecksMeta = { genLatencyMs: 0, refused: 0, raw: 0 }; // R165 per-turn / R174
4335
+ this.specChecksMeta = { genLatencyMs: 0, refused: 0, raw: 0 };
4336
+ this.specChecksPromise = null;
4337
+ this.specFailHistory.clear();
4338
+ this.lastHoldMs = 0;
4339
+ this.derivationHolds = 0; // R165 per-turn / R174 / R173c / R176
4219
4340
  this.recordDsmlRecovery(currentAssistantCanonicalMessage);
4220
4341
  const assistantMessageId = currentAssistantCanonicalMessage.uuid;
4221
4342
  const assistantMessage = {
@@ -5014,6 +5135,7 @@ export class CortexOrchestrator {
5014
5135
  await this.deliverDeferredCorpusAtLift(effectiveModel);
5015
5136
  this.deliverLiftNudge(allTools, effectiveModel); // A′ proposal-1: SearchTools/AskForAdvice signpost at lift
5016
5137
  await this.deliverLiftPlanAtLift(effectiveModel); // LIFT_MENTOR_PLANNER: bounded mentor-planner at lift (dark unless CORTEX_LIFT_PLAN)
5138
+ this.maybeAuthorSpecChecksAtLift(); // R174b: blind spec checks authored in the background at lift (CORTEX_JUDGE_SPEC_TESTS_AT=lift)
5017
5139
  if (this.config.debug)
5018
5140
  console.log('[Anchor] lifted at first tool_result boundary — session profile applies');
5019
5141
  }