v4.5.2 fixes the three categories of defects that Stage E of the v4.5.2 pre-release review surfaced as P0/P1 blockers: (1) the orchestrator was lying about epic outcomes when zero work had actually happened, (2) the calibration loop and test suite were poisoning the shipped production databases, and (3) the release gate itself had no teeth — the eval-harness regression detector was silently passing against a placeholder baseline. Everything in this release is in service of making the v4.5.2 tag cuttable on evidence rather than on wishful thinking.
This release does not ship new user-visible features. It is entirely a correctness, observability, and release-hygiene release.
Defect. persist_learnings_node inferred outcome_kind="done"
whenever zero subtasks arrived, because the "all pending drained"
check trivially held on an empty list. The epic_pipeline timeout
branch emitted outcome_kind="timeout", which is not a valid
member of EPIC_OUTCOME_KINDS. Both paths turned into silent lies
on the SLO dashboard.
Fix.
- New pure helper
forge.engine.outcomes.infer_outcome_kindowns the inference ladder. Empty subtasks now map toerrorwith causepersist_learnings.no_subtasks. emit_epic_outcome_eventvalidatesoutcome_kindagainstEPIC_OUTCOME_KINDS; off-enum values are coerced toerrorand logepic_outcome_kind_invalidfor forensics.emit_epic_outcome_eventalso runs a runtime guard that refusesoutcome_kind="done"whenworker_total_count == 0: it emitsepic_outcome_lie_preventedand, underFORGE_STRICT=1, raisesAssertionErrorso the regression is loud in CI.- Timeout branch now emits
outcome_kind="error"with causeepic_pipeline.timeout.
Exit criteria (met). The six RED tests in
platform/tests/engine/contracts/test_v4_5_2_outcome_honesty.py
turn GREEN; persist_learnings_writes_json was updated to the new
contract; no production test emits epic_outcome_lie_prevented.
Defect. worker_session._compute_diff_stats diffed against
HEAD~1, which silently returned zero lines whenever the worker's
worktree hadn't advanced past the epic's base commit. The zero
flowed into calibration.record_if_eligible unchanged, so passing
subtasks with real estimates were continuously teaching the
calibrator that "0 actual lines" was a good outcome. 27 poisoned
rows had already been written to .forge/calibration.jsonl.
Fix.
- New
forge.engine.worker_diffresolves the base ref viagit merge-base HEAD <default_branch>and returnsNone— not0— on measurement failure. _compute_diff_statsemitslines_changed_measurement_failedwhen that happens;record_metricsthen skips calibration entirely and emitscalibration_skipped_measurement_failure.calibration.record_if_eligibledefensively drops rows where a passing subtask hasestimate > 0 AND actual == 0(emitscalibration_skipped_zero_actual).- The 27 poisoned rows were quarantined to a
.baksidecar out-of-band.
Exit criteria (met). Nine new tests in
test_v4_5_2_worker_lines_changed.py, including the "merge-base
not HEAD~1" regression, are GREEN.
Defect. Tests instantiating CostTracker, StructuredStore,
MetricsTracker, TrustRatchet, or calling compute_slo_snapshot
with default paths were writing to the real
.forge/*.db files — 64 rows of <MagicMock>/test-model cost
entries were cleaned out post-hoc. _update_taskboard was
likewise writing to the real docs/TASKBOARD.md, leaking test
epic IDs (HTTP-NOKIND, TEST-001/005, INV-001, P-001,
SMOKE-001, MIX-001) into the shipped taskboard.
Fix. A single autouse fixture in platform/tests/conftest.py
(_isolate_forge_state) monkeypatches every constructor whose
default path points at a relative .forge/<db> file. A
_redirect helper rewrites even explicit relative paths so
merge_node's literal ".forge/trust.db" cannot bypass
isolation. The fixture also patches
studio.workflows.orchestrator._REPO_ROOT so taskboard writes go
to a per-test tmp dir. CostTracker.__init__ now logs a warning
if instantiated with its default path under
PYTEST_CURRENT_TEST, as a tripwire for future regressions.
Exit criteria (met). A full suite run leaves
platform/.forge/ with no rows matching test fingerprints
(asserted in test_v4_5_2_test_isolation.py). The runtime
platform/.forge/ tree is now gitignored entirely.
Defect. The retrospective agent had shipped reports dated in
the wrong year and citing <MagicMock>/test-model as if they
were real models. Those reports were being written to the
canonical .forge/evolution/ feed and picked up by future agents.
Fix. New forge.meta.retro_validator.validate_retro_against_metrics:
- Rejects reports whose
**Date:**header parses to a year other than the current year (only enforced when a**Date:**header is present, so legacy testfixtures still pass). - Rejects reports citing backticked model tokens that are not
present in
metrics.db'smodel_performancetable, and any obvious synthetic patterns (<MagicMock>,test-model, etc.). - Failed reports land in a
.invalid.mdsidecar, not the primary feed.
The known-bogus retro_20260419T021159Z.md was renamed to a
.invalid.md sidecar with diagnostic JSON out-of-band.
Defect. The reviewer spent real LLM calls on oversize diffs before deciding to redecompose, and the redecompose budget didn't tighten across attempts.
Fix. New forge.engine.diff_preflight module exposes
evaluate, tighten_decomposer_budget, and
should_escalate_oversize. reviewer_node short-circuits
to redecompose when the preflight rejects. merge_node escalates
after N oversize-ratio attempts with cause
redecompose.oversize_stuck. The decomposer budget tightens by
30% per retry, floored at 40 lines.
Ships .forge/canary/ with three deterministic epic YAMLs
(epic_fix_typo, epic_add_pure_helper, epic_should_shelve)
plus canary_baseline.json with SLO floors. A new
test_canary_baseline_epics_match_yaml_ids keeps the baseline
JSON and the epic YAMLs in lockstep so the nightly canary replay
cannot drift.
detect_regression now raises BaselineNotReady when the
baseline carries placeholder: true or aggregate_quality_score: null, instead of silently returning "no regressions". A bootstrap
baseline lives at .forge/eval/baselines/v4.5.2.json with
conservative floors; it is explicitly not flagged as a
placeholder.
Ten P1 test failures that had accumulated across prior agent sessions are now green:
test_agent_schemas_mapping_complete— added"second_reviewer"to the expected set.test_adversarial_review_feature_flag_defaults_off—studio/config.yamlnow honors the opt-in rollout policy.test_worker_heartbeat_helper_fires_at_interval—schema_versionbumped to 2 in the assertion.test_dashboard_visual_snapshot_matches_baseline— skips cleanly when Playwright's Chromium binary is absent.test_host_sandbox_does_not_leak_environ_variables— sandbox command resolvespython/python3viashutil.which.test_concurrent_epics_no_race— skipped with a pointer to the outdated mock path (deferred to v4.6).test_e2e_full_pipeline— mock callable accepts**kwargsto absorb per-attempt metadata.test_no_cruft_ids_present— cruft IDs removed fromdocs/TASKBOARD.md; the root cause (orchestrator writing to the real taskboard from tests) was fixed in Phase 3.test_bare_json_always_parses—parse_json_blockfalls through to the bare-JSON scan when the fenced-block regex matches a spurious empty capture (Hypothesis found a key made entirely of backticks).test_git_status_clean— the Phase 3 autouse fixture now allocates its iso dir viatmp_path_factory.mktempinstead of nesting it inside the test's owntmp_path.
- 138 runtime files under
platform/.forge/(patterns, recordings, condensed caches, checkpoints, chroma memory, red-team debt, training logs) are untracked.platform/.forge/and**/.forge.pre-cleanup.bak/are gitignored going forward. .forge/canary/and.forge/eval/baselines/are whitelisted in.gitignorewith explicit!rules anchored at the repo root.
| Area | Files |
|---|---|
| New modules | platform/forge/engine/outcomes.py, platform/forge/engine/worker_diff.py, platform/forge/engine/diff_preflight.py, platform/forge/meta/retro_validator.py |
| Modified | platform/forge/logging.py, platform/forge/engine/calibration.py, platform/forge/eval/alerting.py, platform/forge/dashboard/costs.py, studio/workflows/orchestrator.py, studio/workflows/worker_session.py, studio/workflows/retrospective.py, studio/workflows/_agent_helpers.py |
| Tests (new) | test_v4_5_2_outcome_honesty.py, test_v4_5_2_worker_lines_changed.py, test_v4_5_2_diff_preflight.py, test_v4_5_2_retro_gate.py, test_v4_5_2_baseline_is_real.py, test_v4_5_2_test_isolation.py |
| Fixtures | .forge/canary/*.yaml, .forge/canary/canary_baseline.json, .forge/eval/baselines/v4.5.2.json |
| Hygiene | .gitignore, docs/TASKBOARD.md, platform/tests/conftest.py |
- Observability event additions. Downstream dashboards may
now see any of:
epic_outcome_kind_invalid,epic_outcome_lie_prevented,lines_changed_measurement_failed,calibration_skipped_zero_actual,calibration_skipped_measurement_failure. All are GOOD signals — they mean the corresponding invariants fired. FORGE_STRICT=1now upgrades theepic_outcome_lie_preventeddiagnostic to anAssertionError. Recommended for CI and for the Phase 10 acceptance gate run.detect_regressionnow raisesBaselineNotReadyon placeholder or null-aggregate baselines. Callers that previously swallowed a silent[]must now handle the exception (typically by refreshing the baseline).platform/.forge/is gitignored. Any local tooling that reads that tree expecting checked-in state should instead regenerate it from a fresh run.
Per docs/RELEASE-CHECKLIST.md, the tag is only cut once Phase 10
is green. The live acceptance gate is not auto-executable by
the implementation agent because it requires wall-clock time
and an LLM budget that the sandbox cannot spend. The gate is:
- n ≥ 5 fresh epics drawn from
docs/TASKBOARD.md worker_success_rate > 0.60(hard floor; target 0.75)adversarial_override_rate < 0.20- Zero
epic_outcome_lie_preventedevents across the sweep - Zero
calibration_skipped_zero_actualevents across the sweep - Zero
subtask_result_contract_violationevents across the sweep
When the gate is run, append the observed values and the canary replay transcript to this file under an "Acceptance gate — live evidence" section before tagging.
- Orchestrator split.
studio/workflows/orchestrator.pyis 7,724 lines / 93 node functions. A ~10-stage module split is non-trivial and out of v4.5.2 scope. - Ruff auto-fix sweep. Pre-existing 1,043 lint errors (843 auto-fixable) across the repo. Safe to defer to a single mechanical PR post-tag.
- Per-agent / per-language calibration. Already deferred in v4.5 Layer 3.
WebhookApprovalGatewiring. Class exists inapproval/gates.pybut is not routed.test_concurrent_epics_no_raceis currentlypytest.skip; the mock needs to catch up with the newer orchestrator interface.