@miller-tech/uap 1.213.0 → 1.215.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +2 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/deliver.d.ts +66 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +227 -7
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/verify.d.ts +5 -0
- package/dist/cli/verify.d.ts.map +1 -1
- package/dist/cli/verify.js +50 -1
- package/dist/cli/verify.js.map +1 -1
- package/dist/cli/wizard-config.d.ts.map +1 -1
- package/dist/cli/wizard-config.js +26 -1
- package/dist/cli/wizard-config.js.map +1 -1
- package/dist/delivery/await-run.d.ts +47 -0
- package/dist/delivery/await-run.d.ts.map +1 -1
- package/dist/delivery/await-run.js +392 -2
- package/dist/delivery/await-run.js.map +1 -1
- package/dist/delivery/capability-profile.d.ts +124 -0
- package/dist/delivery/capability-profile.d.ts.map +1 -0
- package/dist/delivery/capability-profile.js +392 -0
- package/dist/delivery/capability-profile.js.map +1 -0
- package/dist/delivery/convergence-loop.d.ts +38 -0
- package/dist/delivery/convergence-loop.d.ts.map +1 -1
- package/dist/delivery/convergence-loop.js +98 -1
- package/dist/delivery/convergence-loop.js.map +1 -1
- package/dist/delivery/epic-controller.d.ts +9 -0
- package/dist/delivery/epic-controller.d.ts.map +1 -1
- package/dist/delivery/epic-controller.js +1 -0
- package/dist/delivery/epic-controller.js.map +1 -1
- package/dist/delivery/epic-mission.d.ts +10 -2
- package/dist/delivery/epic-mission.d.ts.map +1 -1
- package/dist/delivery/epic-mission.js +1 -1
- package/dist/delivery/epic-mission.js.map +1 -1
- package/dist/delivery/escalation.d.ts +8 -0
- package/dist/delivery/escalation.d.ts.map +1 -1
- package/dist/delivery/escalation.js +8 -1
- package/dist/delivery/escalation.js.map +1 -1
- package/dist/delivery/execution-gate.d.ts +29 -0
- package/dist/delivery/execution-gate.d.ts.map +1 -1
- package/dist/delivery/execution-gate.js +237 -9
- package/dist/delivery/execution-gate.js.map +1 -1
- package/dist/delivery/mission-acceptance.d.ts +4 -0
- package/dist/delivery/mission-acceptance.d.ts.map +1 -1
- package/dist/delivery/mission-acceptance.js +35 -1
- package/dist/delivery/mission-acceptance.js.map +1 -1
- package/dist/delivery/run-state.d.ts +31 -0
- package/dist/delivery/run-state.d.ts.map +1 -1
- package/dist/delivery/run-state.js +49 -0
- package/dist/delivery/run-state.js.map +1 -1
- package/dist/mcp-router/tools/deliver.d.ts.map +1 -1
- package/dist/mcp-router/tools/deliver.js +5 -2
- package/dist/mcp-router/tools/deliver.js.map +1 -1
- package/package.json +1 -1
- package/scripts/lib/llama-upstream.sh +64 -16
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/confidence_escalation.py +57 -0
- package/tools/agents/tests/test_confidence_escalation.py +140 -2
package/dist/cli/deliver.js
CHANGED
|
@@ -27,6 +27,7 @@ import { authorAcceptanceGate } from '../delivery/self-gate.js';
|
|
|
27
27
|
import { applyPendingIntents } from '../delivery/pending-intents.js';
|
|
28
28
|
import { runAcceptanceGate } from '../delivery/acceptance-judge.js';
|
|
29
29
|
import { buildMissionAcceptanceGate, resolveAcceptanceVerdict } from '../delivery/mission-acceptance.js';
|
|
30
|
+
import { compareCapability, invalidateCapabilityBaseline, readCapabilityBaseline, readCapabilityCurrent, writeCapabilityBaseline, fpsOf, drawsPerFrame, } from '../delivery/capability-profile.js';
|
|
30
31
|
import { createSpecRegistry } from '../delivery/spec-registry.js';
|
|
31
32
|
import { guardAgainstOwnerExit, resolveOwnerPid } from '../delivery/orphan-guard.js';
|
|
32
33
|
import { findRepoRoot, formatScopeNotice, unreachablePaths } from '../delivery/scope-notice.js';
|
|
@@ -187,7 +188,7 @@ export function bestKeepFastScore(gateResults, fastRungIds) {
|
|
|
187
188
|
export { resolveAcceptanceVerdict } from '../delivery/mission-acceptance.js';
|
|
188
189
|
import { createAgenticExecutor, noopApplier, selectExecutorMode, lockContractFiles } from '../delivery/agentic-executor.js';
|
|
189
190
|
import { createRepairEscalation } from '../delivery/repair-escalation.js';
|
|
190
|
-
import { clearProjectStop, clearStop, finalRunStatus, isRunBudgetExpired, isStopRequested, makeStopLatch, requestStop, runBudgetMinutes, loadRunState, newRunId, saveRunState, isEpicResume, MAX_PERSISTED_PHASES } from '../delivery/run-state.js';
|
|
191
|
+
import { clearProjectStop, clearStop, finalRunStatus, isRunBudgetExpired, isRunBudgetExplicit, isStopRequested, makeStopLatch, phaseAwareRunBudgetMinutes, requestStop, runBudgetMinutes, loadRunState, newRunId, saveRunState, isEpicResume, MAX_PERSISTED_PHASES } from '../delivery/run-state.js';
|
|
191
192
|
import { planDeliveryPhases, shouldDecompose } from '../delivery/decompose.js';
|
|
192
193
|
import { initLedger, markItem } from '../delivery/completion-ledger.js';
|
|
193
194
|
import { loadUapConfigRaw } from '../utils/config-loader.js';
|
|
@@ -272,7 +273,7 @@ import { createHaloDeliveryTracer } from '../delivery/halo-trace.js';
|
|
|
272
273
|
import { haloTracePath, isHaloTracingEnabled } from '../observability/halo-exporter.js';
|
|
273
274
|
import { createRunCoordinator } from '../delivery/run-coordinator.js';
|
|
274
275
|
import { readCuratedIdeas } from './ideate.js';
|
|
275
|
-
import { createEscalationController, defaultEscalationLadder } from '../delivery/escalation.js';
|
|
276
|
+
import { createEscalationController, defaultEscalationLadder, DEFAULT_STAGNATION_TURNS } from '../delivery/escalation.js';
|
|
276
277
|
import { FilePracticeStore, defaultPracticePath, extractKeywords, retrievePracticesSemantic, } from '../delivery/practice.js';
|
|
277
278
|
import { detectRungs, mergeRedetectedRungs, runLadder, runTieredLadder, tierOf, TIER_ORDER, demoteBaselineFailures, baselineRegressions } from '../delivery/verifier-ladder.js';
|
|
278
279
|
import { runDeployDevLadder } from '../delivery/deploy-dev-gate.js';
|
|
@@ -1126,6 +1127,91 @@ export async function deliverCommand(instruction, options) {
|
|
|
1126
1127
|
throw err;
|
|
1127
1128
|
}
|
|
1128
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Consecutive flat turns after which an epic attempt yields to a fresh one.
|
|
1132
|
+
*
|
|
1133
|
+
* TWO, from the measurement rather than from taste. In every flat attempt
|
|
1134
|
+
* observed (rust-pg-ext, qwen3.8-27b, 2026-08-20) the score was already pinned
|
|
1135
|
+
* by turn 2 and never moved again through turn 5 — and `↑ turn 3: enable
|
|
1136
|
+
* critic` fired in all four of them without changing anything. Turn 1 can never
|
|
1137
|
+
* trip it: the first score is by definition an improvement on "no score yet".
|
|
1138
|
+
*
|
|
1139
|
+
* Only ever applied when a retry remains, so the cost of being wrong is one
|
|
1140
|
+
* fresh attempt on the same epic — the thing that actually rescued the measured
|
|
1141
|
+
* case (a flat 5-turn attempt, then a pass in 2).
|
|
1142
|
+
*/
|
|
1143
|
+
/**
|
|
1144
|
+
* Consecutive flat turns after which an epic attempt yields to a fresh one.
|
|
1145
|
+
*
|
|
1146
|
+
* THREE, and the number is measured rather than chosen. Replaying this rail's
|
|
1147
|
+
* exact streak logic over 157 deliver logs — 51 attempts where it would arm —
|
|
1148
|
+
* gives, per threshold: fires / turns cut / in-attempt breakthroughs destroyed
|
|
1149
|
+
*
|
|
1150
|
+
* K=2 35 101 5 <- the first draft. Destroys five real wins.
|
|
1151
|
+
* K=3 32 67 3
|
|
1152
|
+
* K=4 30 35 2
|
|
1153
|
+
*
|
|
1154
|
+
* Moving 2 -> 3 gives up 34 turns of saving and spares two attempts that went
|
|
1155
|
+
* on to PASS from a flat run — `migration-register-rel-record` [75,75,75,PASS]
|
|
1156
|
+
* and `contracts` [100,100,100,PASS]. The first draft's comment asserted that a
|
|
1157
|
+
* turn-4 breakthrough was something "this sample never showed"; the corpus
|
|
1158
|
+
* contains one, and it passed. That claim was wrong and this is the correction.
|
|
1159
|
+
*
|
|
1160
|
+
* The count is a FLOOR, scaled up with the turn budget by the loop (see
|
|
1161
|
+
* `abortOnFlatTurns`): every breakthrough still lost at K=3 is a 6-8 turn
|
|
1162
|
+
* attempt from a maxTurns=20 run, where three flat turns is a far weaker signal
|
|
1163
|
+
* than it is out of five.
|
|
1164
|
+
*
|
|
1165
|
+
* Caveat kept in view: the replay reads gate percentages from logs, so it
|
|
1166
|
+
* cannot see acceptance movement or inconclusive turns — both of which reset
|
|
1167
|
+
* the streak. Every "destroyed" count above is therefore an upper bound.
|
|
1168
|
+
*/
|
|
1169
|
+
export const EPIC_FLAT_TURN_YIELD = 3;
|
|
1170
|
+
/**
|
|
1171
|
+
* The threshold, with an operator override: `UAP_EPIC_FLAT_TURN_YIELD`.
|
|
1172
|
+
* `0` or any non-positive value disables the rail entirely.
|
|
1173
|
+
*
|
|
1174
|
+
* Blank is treated as UNSET, not as zero: an exported-but-empty variable is a
|
|
1175
|
+
* common accident, and `Number('')` is 0, which would silently switch the rail
|
|
1176
|
+
* off — the exact inversion of the "a typo must not disable it" intent.
|
|
1177
|
+
*/
|
|
1178
|
+
export function epicFlatTurnYield() {
|
|
1179
|
+
const raw = process.env.UAP_EPIC_FLAT_TURN_YIELD;
|
|
1180
|
+
if (raw !== undefined && raw.trim() !== '') {
|
|
1181
|
+
const n = Number(raw);
|
|
1182
|
+
if (Number.isFinite(n))
|
|
1183
|
+
return n >= 1 ? Math.floor(n) : undefined;
|
|
1184
|
+
}
|
|
1185
|
+
return EPIC_FLAT_TURN_YIELD;
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* The loop option that yields a FLAT epic attempt to a fresh one — or nothing.
|
|
1189
|
+
*
|
|
1190
|
+
* Extracted so the WIRING is testable, not just the threshold. It carries the
|
|
1191
|
+
* whole safety argument: ending an attempt early is defensible only while a
|
|
1192
|
+
* fresh attempt remains, so on the last attempt (`retriesRemaining` 0 or
|
|
1193
|
+
* absent) this returns `{}` and the attempt plays out exactly as today.
|
|
1194
|
+
*
|
|
1195
|
+
* WHAT THIS DOES AND DOES NOT BUY, stated precisely because the first draft
|
|
1196
|
+
* overclaimed it: no turn budget is transferred. Each attempt gets its own
|
|
1197
|
+
* `maxTurns` either way, so a fresh attempt is not made longer by an earlier
|
|
1198
|
+
* one ending sooner — the epic's TOTAL exploration shrinks. What is saved is
|
|
1199
|
+
* wall clock, on turns whose gate score is provably not moving.
|
|
1200
|
+
*/
|
|
1201
|
+
export function epicFlatTurnOption(retriesRemaining, escalationLadderReach = 0) {
|
|
1202
|
+
if (!((retriesRemaining ?? 0) > 0))
|
|
1203
|
+
return {};
|
|
1204
|
+
const limit = epicFlatTurnYield();
|
|
1205
|
+
if (limit === undefined)
|
|
1206
|
+
return {};
|
|
1207
|
+
// NEVER yield while this attempt still has an unplayed escalation tier — the
|
|
1208
|
+
// ladder is precisely "what this attempt has left to try", and cutting it is
|
|
1209
|
+
// not reclaiming waste, it is skipping the rescue. Raising the threshold to
|
|
1210
|
+
// the ladder's reach is self-correcting: when the ladder needs the whole turn
|
|
1211
|
+
// budget (a stronger model on a 5-turn attempt), the resulting limit exceeds
|
|
1212
|
+
// the budget and the rail simply never fires.
|
|
1213
|
+
return { abortOnFlatTurns: Math.max(limit, escalationLadderReach) };
|
|
1214
|
+
}
|
|
1129
1215
|
async function runDeliver(instruction, options) {
|
|
1130
1216
|
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
1131
1217
|
// P1 (plan D1): --pending replays gate-recorded edit intents exactly, then
|
|
@@ -2052,6 +2138,32 @@ async function runDeliver(instruction, options) {
|
|
|
2052
2138
|
onEscalate: (tier, turn) => console.log(chalk.magenta(` ↑ turn ${turn}: ${tier.label}`)),
|
|
2053
2139
|
})
|
|
2054
2140
|
: undefined;
|
|
2141
|
+
/**
|
|
2142
|
+
* The turn on which the LAST escalation tier issues its directive.
|
|
2143
|
+
*
|
|
2144
|
+
* An epic attempt may end early on a plateau, and that must never cut the
|
|
2145
|
+
* ladder short: a tier decided on turn N is consumed by turn N+1, so yielding
|
|
2146
|
+
* at or before the last tier's turn means the tier never runs. Measured
|
|
2147
|
+
* against the real ladder — with a stronger model configured, an attempt that
|
|
2148
|
+
* delivers on turn 6 was abandoned on turn 4 and the stronger model was never
|
|
2149
|
+
* called once.
|
|
2150
|
+
*
|
|
2151
|
+
* 0 when no ladder is active, which is the configuration the plateau
|
|
2152
|
+
* measurement was taken in (those logs show `enable critic` and nothing
|
|
2153
|
+
* above it — no stronger model was configured).
|
|
2154
|
+
*/
|
|
2155
|
+
const escalationLadderReach = (() => {
|
|
2156
|
+
if (!options.escalate)
|
|
2157
|
+
return 0;
|
|
2158
|
+
const tiers = defaultEscalationLadder({
|
|
2159
|
+
includeExploration: !agentic,
|
|
2160
|
+
candidates,
|
|
2161
|
+
maxTurns: maxTurns ?? 5,
|
|
2162
|
+
escalateExecutor,
|
|
2163
|
+
escalateModelName,
|
|
2164
|
+
}).length;
|
|
2165
|
+
return tiers > 0 ? DEFAULT_STAGNATION_TURNS * tiers + 1 : 0;
|
|
2166
|
+
})();
|
|
2055
2167
|
// Phase 4: learned best-practice cards for similar tasks. Retrieval is
|
|
2056
2168
|
// semantic (embeddings) by default — better recall than keyword overlap —
|
|
2057
2169
|
// and falls back to keyword matching automatically when no embedding
|
|
@@ -2282,6 +2394,24 @@ async function runDeliver(instruction, options) {
|
|
|
2282
2394
|
const fastScore = bestKeepFastScore(record.gateResults, bestFastRungIds);
|
|
2283
2395
|
if (fastScore === null || fastScore <= baselineGateScore)
|
|
2284
2396
|
return; // not a new best
|
|
2397
|
+
// VETO on capability. This is the whole reason the capability profile exists,
|
|
2398
|
+
// and without this hook it would not have closed the incident it cites: on
|
|
2399
|
+
// the recorded regression the gate score went 0% -> 100% WHILE the app lost
|
|
2400
|
+
// most of its function. Acceptance now blocks that turn — but acceptance is
|
|
2401
|
+
// not a rung, so its verdict never reaches `record.gateResults`, and this
|
|
2402
|
+
// hook would happily snapshot the gutted tree as the run's new "best" and
|
|
2403
|
+
// DISPOSE the good one. The end-of-run rollback then restores the damage.
|
|
2404
|
+
//
|
|
2405
|
+
// Reads the same two files acceptance does, so the two cannot disagree.
|
|
2406
|
+
const capCmp = compareCapability(readCapabilityBaseline(projectRoot), readCapabilityCurrent(projectRoot));
|
|
2407
|
+
if (capCmp.regressed) {
|
|
2408
|
+
if (!options.dryRun) {
|
|
2409
|
+
console.log(chalk.yellow(` no-regress: turn scored ${fastScore.toFixed(2)} but LOST capability — refusing to adopt it as best`));
|
|
2410
|
+
for (const f of capCmp.findings)
|
|
2411
|
+
console.log(chalk.dim(` ${f}`));
|
|
2412
|
+
}
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2285
2415
|
const snap = snapshotTree(projectRoot);
|
|
2286
2416
|
if (!snap.ok)
|
|
2287
2417
|
return; // size-cap / failure: keep the previous best
|
|
@@ -2682,7 +2812,26 @@ async function runDeliver(instruction, options) {
|
|
|
2682
2812
|
// 2026-08-13 — 651 minutes, still phase 0 of 6, 20% of gates on every turn).
|
|
2683
2813
|
// Riding the stop latch means expiry behaves exactly like an operator's stop
|
|
2684
2814
|
// request: the turn finishes, the work is checkpointed, the lock is released.
|
|
2685
|
-
|
|
2815
|
+
// The budget is captured BEFORE decomposition, so it starts at the calibrated
|
|
2816
|
+
// single-mission default and is re-derived once the plan's size is known (see
|
|
2817
|
+
// applyPhaseBudget). `let`, because a 7-epic plan needs a different clock than
|
|
2818
|
+
// the one-mission runs the 120-minute default was measured on.
|
|
2819
|
+
const budgetExplicit = isRunBudgetExplicit(projectRoot);
|
|
2820
|
+
let budgetMinutes = runBudgetMinutes(projectRoot);
|
|
2821
|
+
const baseBudgetMinutes = budgetMinutes;
|
|
2822
|
+
/**
|
|
2823
|
+
* Grant the run time proportional to the plan, once the plan exists.
|
|
2824
|
+
* No-op when the operator set the budget themselves, and never shrinks it.
|
|
2825
|
+
*/
|
|
2826
|
+
const applyPhaseBudget = (phaseCount) => {
|
|
2827
|
+
if (budgetExplicit)
|
|
2828
|
+
return;
|
|
2829
|
+
const next = phaseAwareRunBudgetMinutes(baseBudgetMinutes, phaseCount);
|
|
2830
|
+
if (next > budgetMinutes) {
|
|
2831
|
+
budgetMinutes = next;
|
|
2832
|
+
console.log(chalk.dim(` ⏱ run budget ${baseBudgetMinutes} → ${budgetMinutes} min for ${phaseCount} planned phases`));
|
|
2833
|
+
}
|
|
2834
|
+
};
|
|
2686
2835
|
const runStartedAtMs = Date.now();
|
|
2687
2836
|
let stoppedForBudget = false;
|
|
2688
2837
|
// Captured at OBSERVATION time: isStopRequested consumes the project-level
|
|
@@ -3021,6 +3170,14 @@ async function runDeliver(instruction, options) {
|
|
|
3021
3170
|
persistPlan: (plan) => {
|
|
3022
3171
|
runState.phases = plan;
|
|
3023
3172
|
saveRunState(runState);
|
|
3173
|
+
// THE epic budget hook. On a fresh epic run — the DEFAULT for every
|
|
3174
|
+
// mission — the outer `phases` is undefined (it is only ever
|
|
3175
|
+
// resumeState.phases; the decompose block is explicitly skipped when
|
|
3176
|
+
// epics are on), so the call site before dispatch never fires and the
|
|
3177
|
+
// whole scaler was dead on the path that motivated it. The plan first
|
|
3178
|
+
// exists HERE. Re-granting on a later re-plan is safe: the grant only
|
|
3179
|
+
// ever raises (see phaseAwareRunBudgetMinutes).
|
|
3180
|
+
applyPhaseBudget(plan.length);
|
|
3024
3181
|
},
|
|
3025
3182
|
persistCompleted: ({ summaries, completed }) => {
|
|
3026
3183
|
runState.phaseSummaries = [...summaries];
|
|
@@ -3040,8 +3197,14 @@ async function runDeliver(instruction, options) {
|
|
|
3040
3197
|
saveRunState(runState);
|
|
3041
3198
|
},
|
|
3042
3199
|
runOrchestrated: (missionText, plan, parentTaskId) => runOrchestratedMissionCore(buildOrchestratedDeps(missionText, plan, epicParallelTasks, parentTaskId)),
|
|
3043
|
-
runEpicLoop: async (scoped) => {
|
|
3044
|
-
const loop = new ConvergenceLoop({
|
|
3200
|
+
runEpicLoop: async (scoped, opts) => {
|
|
3201
|
+
const loop = new ConvergenceLoop({
|
|
3202
|
+
...loopConfig,
|
|
3203
|
+
baselineCheck: false,
|
|
3204
|
+
resumeFrom: undefined,
|
|
3205
|
+
onIteration: makeIterationHook(),
|
|
3206
|
+
...epicFlatTurnOption(opts?.retriesRemaining, escalationLadderReach),
|
|
3207
|
+
}, executor, seams);
|
|
3045
3208
|
return loop.deliver(scoped);
|
|
3046
3209
|
},
|
|
3047
3210
|
setEpicSpec: (spec) => specs.setShared(spec), // fresh epic — breaker needs fresh diff evidence
|
|
@@ -3199,6 +3362,32 @@ async function runDeliver(instruction, options) {
|
|
|
3199
3362
|
const keepBest = resolveKeepBest(options.keepBest) && fastRungs.length > 0 && !needsSelfGate;
|
|
3200
3363
|
let regressSnapshot = null;
|
|
3201
3364
|
let baselineGateScore = 0;
|
|
3365
|
+
// Capability baseline: what the deliverable DOES before this run touches it.
|
|
3366
|
+
//
|
|
3367
|
+
// PROMOTED, not measured. The pre-run ladder immediately below already runs
|
|
3368
|
+
// the execution gate, and that gate writes `.uap/capability/current.json` as a
|
|
3369
|
+
// byproduct of the browser session it opens anyway — so pinning a baseline is
|
|
3370
|
+
// a file copy, not a second headless launch. That is the point of having moved
|
|
3371
|
+
// the measurement into the gate.
|
|
3372
|
+
//
|
|
3373
|
+
// Reported when it is ABSENT, too. An unmeasured baseline silently disables
|
|
3374
|
+
// the whole check, and this repo has shipped inert rungs that looked exactly
|
|
3375
|
+
// like healthy ones from the outside.
|
|
3376
|
+
const pinCapabilityBaseline = () => {
|
|
3377
|
+
if (options.dryRun)
|
|
3378
|
+
return;
|
|
3379
|
+
const pre = readCapabilityCurrent(projectRoot);
|
|
3380
|
+
if (!pre?.measured) {
|
|
3381
|
+
// Nothing measurable this run, so any baseline still on disk describes a
|
|
3382
|
+
// DIFFERENT tree. Drop it rather than measure against it.
|
|
3383
|
+
invalidateCapabilityBaseline(projectRoot);
|
|
3384
|
+
console.log(chalk.dim(` capability: no baseline (${pre?.reason ?? 'not measured'}) — regression check inactive`));
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
writeCapabilityBaseline(projectRoot, pre);
|
|
3388
|
+
console.log(chalk.dim(` capability baseline: ${fpsOf(pre).toFixed(1)} fps, ${drawsPerFrame(pre).toFixed(2)} draws/frame, ` +
|
|
3389
|
+
`${pre.programsUsed} programs, ${pre.listenerTypes} event types`));
|
|
3390
|
+
};
|
|
3202
3391
|
if (keepBest) {
|
|
3203
3392
|
baselineGateScore = runLadder(fastRungs, projectRoot).score;
|
|
3204
3393
|
const snapResult = snapshotTree(projectRoot);
|
|
@@ -3216,6 +3405,18 @@ async function runDeliver(instruction, options) {
|
|
|
3216
3405
|
console.log(chalk.yellow(` no-regress: ${label} — ${snapResult.detail}; rollback disabled for this run`));
|
|
3217
3406
|
}
|
|
3218
3407
|
}
|
|
3408
|
+
// Pin the capability baseline for EVERY run, not only under --keep-best.
|
|
3409
|
+
//
|
|
3410
|
+
// It used to sit inside that branch, which meant --no-keep-best (or
|
|
3411
|
+
// UAP_DELIVER_KEEP_BEST=0, or a self-gated project) left the PREVIOUS run's
|
|
3412
|
+
// baseline on disk. A later, unrelated run then measured against a days-old
|
|
3413
|
+
// reading of a different app and blocked every turn — with the only documented
|
|
3414
|
+
// escape being a flag that did nothing. A baseline nobody pinned this run is
|
|
3415
|
+
// worse than no baseline.
|
|
3416
|
+
//
|
|
3417
|
+
// When --keep-best did not run a ladder there may be no reading yet; the
|
|
3418
|
+
// helper says so and leaves the check inactive rather than guessing.
|
|
3419
|
+
pinCapabilityBaseline();
|
|
3219
3420
|
// Mark this run (and the gate subprocesses it spawns) as deliver-driven so
|
|
3220
3421
|
// the delivery-enforcement policy exempts the sanctioned path. Scoped to the
|
|
3221
3422
|
// loop and RESTORED afterward so a programmatic/long-lived caller doesn't
|
|
@@ -3261,6 +3462,14 @@ async function runDeliver(instruction, options) {
|
|
|
3261
3462
|
console.log(chalk.dim(' lazy attempt did not pass — engaging the full convergence stack'));
|
|
3262
3463
|
}
|
|
3263
3464
|
}
|
|
3465
|
+
// Grant the clock the sequence the plan implies, before any runner starts
|
|
3466
|
+
// consuming it. Epics run as N missions back to back, so the single-mission
|
|
3467
|
+
// default expires mid-sequence (measured: 7 epics, ~16 min each, against a
|
|
3468
|
+
// 120-minute cap). This covers the paths where the plan is ALREADY known —
|
|
3469
|
+
// --no-epics decomposed runs, and epic RESUMES (resumeState.phases). A
|
|
3470
|
+
// fresh epic run has no plan yet and is granted in persistPlan instead.
|
|
3471
|
+
if (phases && phases.length >= 2)
|
|
3472
|
+
applyPhaseBudget(phases.length);
|
|
3264
3473
|
// Resume routing: an epic-kind resume re-enters the epic path (fidelity —
|
|
3265
3474
|
// completed epics carry forward as prior summaries). Everything else takes
|
|
3266
3475
|
// a cursor-honoring sequential path — phased when a phase plan was
|
|
@@ -3437,13 +3646,24 @@ async function runDeliver(instruction, options) {
|
|
|
3437
3646
|
const endLadder = runLadder(fastRungs, projectRoot);
|
|
3438
3647
|
const endScore = endLadder.score;
|
|
3439
3648
|
const endRegressions = baselineRegressions(fastRungs, endLadder.results);
|
|
3440
|
-
|
|
3649
|
+
// A capability regression is a THIRD reason to restore, and without it the veto
|
|
3650
|
+
// above is only half a fix. The incident this feature cites is precisely
|
|
3651
|
+
// "gate score went UP while capability collapsed": the veto correctly refuses
|
|
3652
|
+
// to adopt the gutted turn as best, the run then ENDS on that turn, and this
|
|
3653
|
+
// predicate sees endScore >= baseline with no gate regressions and keeps it —
|
|
3654
|
+
// shipping the damage and disposing the good snapshot. The end-of-run ladder
|
|
3655
|
+
// just re-ran the execution rung, so the reading here is fresh.
|
|
3656
|
+
const endCap = compareCapability(readCapabilityBaseline(projectRoot), readCapabilityCurrent(projectRoot));
|
|
3657
|
+
const endCapabilityLost = endCap.regressed;
|
|
3658
|
+
if (endScore < baselineGateScore || endRegressions.length > 0 || endCapabilityLost) {
|
|
3441
3659
|
try {
|
|
3442
3660
|
restoreTree(projectRoot, regressSnapshot);
|
|
3443
3661
|
console.log(chalk.yellow(endRegressions.length > 0
|
|
3444
3662
|
? ` ↩ no-regress: reverted to best — tests that passed at baseline are failing now: ` +
|
|
3445
3663
|
`${endRegressions.flatMap((r) => r.tests).slice(0, 5).join(', ')}`
|
|
3446
|
-
:
|
|
3664
|
+
: endCapabilityLost
|
|
3665
|
+
? ` ↩ no-regress: reverted to best — the run ended having LOST capability: ${endCap.findings.join('; ')}`
|
|
3666
|
+
: ` ↩ no-regress: reverted to best (end gate score ${endScore.toFixed(2)} < best ${baselineGateScore.toFixed(2)})`));
|
|
3447
3667
|
disposeSnapshot(regressSnapshot);
|
|
3448
3668
|
}
|
|
3449
3669
|
catch (restoreErr) {
|