band-aider-pub/band_aider/loop/stabilization_loop.py
2026-10-17 11:57:37 -07:00

1279 lines
53 KiB
Python

# SPDX-FileCopyrightText: 2026 Art Sea LLC
#
# SPDX-License-Identifier: AGPL-3.0-only
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from itertools import chain
from collections import deque
import json
import logging
from pathlib import Path
from typing import Callable
from band_aider.ai_client import llm_logging
from band_aider.context.alpha_context import AlphaContext
from band_aider.context.beta_context import BetaContext
from band_aider.context.class_meta import ClassMeta, ContentType
from band_aider.context.context_factory import context_factory
from band_aider.context.cot_task_generator import CotTaskRecord
from band_aider.context.resume_state import resume_settings
from band_aider.context.stabilization_context import StabilizationContext
from band_aider.context.provenance import Phase, Provenance
from band_aider.context.working_backlog import (
ChangeType,
ContributorBacklog,
FailedQAItem,
InspectionBacklog,
QABacklog,
RemediationDeduplicationKey,
TestingBacklog,
)
from band_aider.judgement.bi_dependency_discovery_strategy import (
BiDependencyDiscoveryStrategy,
)
from band_aider.judgement.code_editor import CodeEditor
from band_aider.judgement.coverage_strategy import CoverageStrategy
from band_aider.judgement.file_manager import FileManager
from band_aider.judgement.retry_vibe_check import RetryVibeCheck
from band_aider.judgement.test_runner_strategy import TestRunnerStrategy
from band_aider.loop.alpha_loop import AlphaLoop
from band_aider.loop.beta_loop import BetaLoop
from band_aider.loop.loop_gate import LoopGate
from band_aider.loop.mechanical_loop_gate import MechanicalLoopGate
from band_aider.loop.proto_loop import ProtoLoop
from band_aider.loop.stabilization_dtos import (
FrontierBoundary,
RemediationClassification,
RemediationClassificationResult,
)
from band_aider.strategies import LanguageStrategySet
from band_aider.ui.status import publish_loop_status, publish_stabilization_status
@dataclass(init=False)
class StabilizationLoop(ProtoLoop[LoopGate, StabilizationContext]):
"""Own one root story, dependency discovery, frontier expansion, and squash.
Example: ``loop.run()`` drains worker work or performs one visible topology
cadence until current-COT state is ready, exhausted, or legitimately blocked.
"""
language_strategy_set: LanguageStrategySet
bi_dependency_strategy: BiDependencyDiscoveryStrategy | None = field(
default=None,
init=False,
)
test_runner_strategy: TestRunnerStrategy = field(init=False)
coverage_strategy: CoverageStrategy | None = field(default=None, init=False)
aider_worker: CodeEditor | None = field(default=None, init=False)
previous_cot_root_readonly_file: Path | None = field(default=None, init=False)
previous_cot_records: tuple[CotTaskRecord, ...] = field(
default=(),
init=False,
repr=False,
)
future_cot_records: list[CotTaskRecord] = field(
default_factory=list,
init=False,
repr=False,
)
new_files_callback: Callable[..., bool] | None = field(default=None, init=False, repr=False)
provenance: Provenance | None = field(default=None, init=False, repr=False)
def reference_frontiers(self) -> list[list[ClassMeta]]:
"""Return the current and traversed production frontiers, nearest first."""
file_manager = self.context.file_manager
_, bfs_depth = self.context.frontier
return [
[
file_manager.current_src(meta.location) or meta
for meta in self.context.full_topologies[index]
]
for index in range(bfs_depth, -1, -1)
]
def __init__(
self,
gate: LoopGate,
context: StabilizationContext,
language_strategy_set: LanguageStrategySet,
aider_worker: CodeEditor | None = None,
previous_cot_root_readonly_file: Path | None = None,
previous_cot_records: tuple[CotTaskRecord, ...] = (),
future_cot_records: list[CotTaskRecord] | None = None,
provenance: Provenance | None = None,
logger=None,
) -> None:
super().__init__(gate=gate, context=context, logger=logger);
self.language_strategy_set = language_strategy_set;
self.bi_dependency_strategy = None;
self.test_runner_strategy = language_strategy_set[TestRunnerStrategy];
self.coverage_strategy = language_strategy_set[CoverageStrategy];
self.aider_worker = aider_worker;
self.previous_cot_root_readonly_file = previous_cot_root_readonly_file;
self.previous_cot_records = previous_cot_records
self.future_cot_records = list(future_cot_records or []);
self.provenance = provenance
self.__post_init__()
def __post_init__(self) -> None:
"""Initialize scoped FileManager and strategies without replacing the injected gate."""
self._initialize_file_manager()
file_manager = self.context.file_manager
assert file_manager is not None
# A standalone stabilization loop owns its graph when no parent task supplies one.
if self.provenance is None:
self.provenance = Provenance.open(
repo_root=file_manager.repo_root,
repo_exec=file_manager.repo_exec,
resume=resume_settings().hydrate,
)
self._ensure_strategies()
assert self.bi_dependency_strategy is not None
self.context.topology.activate(
cot_index=self.context.cot_index,
file_manager=file_manager,
upstream_file_manager=self.context.upstream_file_manager,
roots=self.context.t0,
future_cot_roots=[record.root for record in self.future_cot_records],
)
self.context.topology.bind_runtime(
self.bi_dependency_strategy,
self.logger,
)
def _direct_tests_for(self, source_files: list[ClassMeta]) -> list[ClassMeta]:
"""Return direct tests connected by the configured bi dependency strategy."""
self._ensure_strategies()
file_manager = self.context.file_manager
assert self.bi_dependency_strategy is not None
return self.bi_dependency_strategy.direct_tests_for(
source_files,
file_manager.test_classes,
file_manager.src_root,
file_manager.test_root,
)
def _upstream_sources_for(self, files: list[ClassMeta]) -> list[ClassMeta]:
"""Return upstream source context through the registered bi strategy."""
self._ensure_strategies()
file_manager = self.context.file_manager
assert self.bi_dependency_strategy is not None
return self.bi_dependency_strategy.upstream_sources_for(
files,
file_manager.src_classes,
file_manager.src_root,
file_manager.test_root,
)
def _topology_cadence(
self,
boundary: FrontierBoundary,
) -> list[ClassMeta]:
"""Pump one active frontier through BiGraph and topology admission."""
topology = self.context.topology
file_manager = self.context.file_manager
strategy = self.bi_dependency_strategy
assert file_manager is not None
assert strategy is not None
# Each completed frontier contributes one authoritative raw BFS slice before admission.
seeds = list(map(
lambda location: boundary.source_by_location[location],
filter(
lambda location: location in boundary.source_by_location,
sorted(boundary.live_seed_locations),
),
))
bfs_slice = strategy.downstream_sources_for(
seeds,
file_manager.src_classes,
file_manager.src_root,
)
topology.enqueue_bfs_slice(bfs_slice, boundary)
# An empty raw slice is still topology evidence and must be reconciled.
return topology.dequeue_topologically(input_complete=True)
def _frontier_boundary(
self,
frontier: list[ClassMeta],
frontier_open_hashes: dict[str, str],
) -> FrontierBoundary:
"""Return HEAD's ordinary seed boundary for one admitted frontier."""
file_manager = self.context.file_manager
frontier_after_hashes = dict(map(
lambda source: (
source.location,
(file_manager.current_src(source.location) or source).get_hash(
file_manager
),
),
frontier,
))
unchanged_locations = set(map(
lambda item: item[0],
filter(
lambda item: frontier_after_hashes.get(item[0]) == item[1],
frontier_open_hashes.items(),
),
))
current_locations = set(map(lambda source: source.location, frontier))
topology = self.context.topology
whitelist_seeds = set(filter(
lambda location: (
location in topology.visited or location in current_locations
),
topology.whitelist,
))
# Keep HEAD's ordinary seed algebra outside deferred-topology admission.
live_seed_locations = (
current_locations - unchanged_locations
) | whitelist_seeds
source_by_location = dict(map(
lambda source: (
source.location,
file_manager.current_src(source.location) or source,
),
frontier,
))
# Whitelist relays may come from an earlier frontier; resolve only those actual seeds.
source_by_location.update(dict(filter(
lambda item: item[1] is not None,
map(
lambda location: (location, file_manager.current_src(location)),
live_seed_locations - source_by_location.keys(),
),
)))
return FrontierBoundary(
current_locations=frozenset(current_locations),
unchanged_locations=frozenset(unchanged_locations),
live_seed_locations=frozenset(live_seed_locations),
source_by_location=source_by_location,
)
def _current_frontier_hashes(self) -> dict[str, str]:
"""Return content hashes for the active topological frontier."""
file_manager = self.context.file_manager
assert file_manager is not None
frontier, _ = self.context.frontier
return dict(map(
lambda source: (source.location, source.get_hash(file_manager)),
frontier,
))
def _log_deferred_topology(
self,
boundary: FrontierBoundary,
) -> None:
"""Add backlog lanes to context-owned topology blockers before the terminal stop."""
snapshot = self.context.topology.unresolved_snapshot(boundary)
backlog = self.context.backlog
queue_items = {
"inspection": backlog.inspection,
"runnable_remediation": backlog.remediation,
"testing": backlog.testing,
"inherited_remediation": list(chain.from_iterable(
self.context.inherited_remediation_backlog.values()
)),
"deferred_remediation": list(chain.from_iterable(
self.context.deferred_remediation_backlog.values()
)),
}
snapshot["queues"] = dict(map(
lambda entry: (
entry[0],
sorted(set(map(
lambda item: item.focused_spotlight.location,
filter(
lambda item: item.focused_spotlight is not None,
entry[1],
),
))),
),
queue_items.items(),
))
self.logger.info(
"Deferred topology unresolved snapshot %s",
json.dumps(snapshot, sort_keys=True),
)
def _grow_deferred_frontier_whitelist_locations(
self,
source_files: list[ClassMeta],
) -> None:
"""Grow durable deferred-guided frontier whitelist evidence."""
handoff = self.context.remaining_deferred_remediation
# A topology bridge is useful only when live source remediation supplied evidence.
if not (handoff.inherited_items or handoff.deferred_items):
return
deferred_remediation = [*handoff.inherited_items, *handoff.deferred_items]
file_manager = self.context.file_manager
assert file_manager is not None
deferred_downstream_by_location = dict(map(
lambda downstream: (downstream.location, downstream),
chain.from_iterable(map(
lambda item: item.downstream_classes,
deferred_remediation,
)),
))
assert self.bi_dependency_strategy is not None
whitelist = self.bi_dependency_strategy.joint_beam_sources_for(
source_files,
list(map(
lambda location: deferred_downstream_by_location[location],
sorted(deferred_downstream_by_location),
)),
file_manager.src_classes,
file_manager.src_root,
)
whitelist_locations = set(map(lambda source: source.location, whitelist))
self.context.topology.whitelist.update(
whitelist_locations
)
def _initialize_file_manager(self) -> FileManager:
"""Initialize the stabilization-scoped FileManager and runtime bindings once."""
if self.context.file_manager is None:
self.context.file_manager = FileManager.from_manager(
self.context.upstream_file_manager
)
file_manager = self.context.file_manager
upstream = self.context.upstream_file_manager
file_manager.upstream_file_manager = upstream
file_manager.parser = upstream.parser
file_manager.client = upstream.client
file_manager.dependency_graph_maintainer = upstream.dependency_graph_maintainer
file_manager.dependency_strategy = upstream.dependency_strategy
file_manager.prompt = self.context.goal
self.context.dependency_graph_maintainer = file_manager.dependency_graph_maintainer
if self.aider_worker is not None:
self.aider_worker.file_manager = file_manager
return file_manager
def _ensure_strategies(self) -> None:
"""Ensure stabilization has runtime strategies while FileManager owns dependency state."""
file_manager = self.context.file_manager
self.context.dependency_graph_maintainer = file_manager.dependency_graph_maintainer
self.bi_dependency_strategy = self.language_strategy_set[
BiDependencyDiscoveryStrategy
]
self.test_runner_strategy = self.language_strategy_set[TestRunnerStrategy]
self.coverage_strategy = self.language_strategy_set[CoverageStrategy]
def _current_classes(self) -> list[ClassMeta]:
"""Return the current class metadata visible to the stabilization story."""
file_manager = self.context.file_manager
return [*file_manager.src_classes, *file_manager.test_classes]
def _enqueue_current_frontier_to_backlog(self) -> None:
"""Resolve closed context and durably enqueue one admitted Alpha frontier."""
frontier, bfs_depth = self.context.frontier
# Depth zero is context-admitted rather than returned by TopologicalLiveliness;
# close it at the same admission boundary used by every later frontier.
self.context.topology.visited.update(
map(lambda source: source.location, frontier)
)
# Preserve pre-seeded root work while allowing reached frontiers to join its queues.
if bfs_depth == 0 and (
self.context.backlog.remediation
or self.context.backlog.inspection
or self.context.backlog.testing
):
self.context.persist()
return
file_manager = self.context.file_manager
strategy = self.bi_dependency_strategy
assert file_manager is not None
assert strategy is not None
# Select one previous branch before the new COT's seed or surrogates enter history.
parent_id = (
self.provenance.parent_for(self.context.t0, strategy)
if bfs_depth == 0 and self.provenance is not None
else None
)
# Every admitted sibling is queued before the transition is persisted.
pending_sources = deque(sorted(
frontier,
key=lambda item: item.location,
))
# Queue every sibling from the admitted layer before persisting its Alpha work.
while pending_sources:
source_file = pending_sources.popleft()
direct_upstream = strategy.upstream_sources_for(
[source_file],
file_manager.src_classes,
file_manager.src_root,
file_manager.test_root,
)
selected_locations = set(map(
lambda upstream: upstream.location,
filter(
lambda upstream: (
upstream.location in self.context.topology.visited
),
direct_upstream,
),
))
root_location = self.context.root_change.location
# Root context carries every declared surrogate when that root is directly relevant.
if root_location in selected_locations:
selected_locations.update(
map(
lambda surrogate: surrogate.location,
self.context.surrogate_roots,
)
)
upstream_classes = list(map(
lambda item: item[1],
filter(
lambda item: item[1] is not None,
map(
lambda location: (
location,
file_manager.current_src(location),
),
sorted(selected_locations),
),
),
))
item = InspectionBacklog(
focused_spotlight=source_file,
summary=(
f"Inspect topology {bfs_depth} "
f"source {source_file.location}."
),
upstream_classes=upstream_classes,
downstream_classes=[source_file],
)
# Existing identities remain authoritative when a queued seed is recovered.
if bfs_depth == 0 and self.provenance is not None:
self.provenance.start(Phase.ALPHA, item, parent_id=parent_id)
self.context.backlog.queue_inspection(item)
self.context.persist()
def _frontier_summary(self, frontier: list[ClassMeta], bfs_depth: int) -> str:
"""Return a compact summary for one topologically admitted source frontier."""
locations = ", ".join(class_meta.location for class_meta in frontier)
if not locations:
locations = "no source files"
return f"Topology {bfs_depth} source frontier: {locations}."
def _create_contributor_backlog(self) -> ContributorBacklog:
"""Create Alpha's contributor-facing backlog presentation layer."""
file_manager = self.context.file_manager
return ContributorBacklog(
backlog=self.context.backlog,
get_tests=self._direct_tests_for,
file_manager=file_manager,
parser=file_manager.parser,
language_strategy_set=self.language_strategy_set,
previous_cot_root_readonly_file=self.previous_cot_root_readonly_file,
previous_cot_records=self.previous_cot_records,
)
@staticmethod
def _record_queue_depths(context: StabilizationContext, alpha: AlphaContext | None = None, beta: BetaContext | None = None) -> None:
"""Capture temporary alpha/beta queue depths for terminal status publishing."""
if alpha is None:
context.alpha_inbound_queue_depth = 0
context.alpha_outbound_queue_depth = 0
else:
context.alpha_inbound_queue_depth = len(alpha.inbound_queue)
context.alpha_outbound_queue_depth = len(alpha.outbound_queue)
if beta is None:
context.beta_inbound_queue_depth = 0
context.beta_outbound_queue_depth = 0
else:
context.beta_inbound_queue_depth = len(beta.inbound_queue)
context.beta_outbound_queue_depth = len(beta.outbound_queue)
def _publish_queue_depth_snapshot(
self,
*,
alpha: AlphaContext | None = None,
beta: BetaContext | None = None,
) -> None:
"""Refresh stabilization queue-depth status from the live alpha/beta contexts."""
self._record_queue_depths(self.context, alpha=alpha, beta=beta)
publish_stabilization_status(self.context)
def _create_beta_loop(self) -> BetaLoop:
"""Create BetaLoop as Stabilization's sibling phase after Alpha."""
file_manager = self.context.file_manager
backlog = QABacklog(
backlog=self.context.backlog,
ai_client=file_manager.client,
file_manager=file_manager,
parser=file_manager.parser,
previous_cot_records=self.previous_cot_records,
)
queue_depth_callback = lambda: self._publish_queue_depth_snapshot(beta=beta_context)
should_thaw = False
if resume_settings().hydrate:
payload = context_factory.latest_payload_for(BetaContext) or {}
should_thaw = (
payload.get("goal") == self.context.goal
and bool(
payload.get("active_inbound")
or payload.get("outbound_queue")
or payload.get("inbound_queue")
)
)
if should_thaw:
try:
beta_context = context_factory.thaw(
BetaContext,
goal=self.context.goal,
classes=self._current_classes(),
file_manager=file_manager,
backlog=backlog,
runtime_queue_depth_callback=queue_depth_callback,
)
except FileNotFoundError:
should_thaw = False
if not should_thaw:
beta_context = context_factory.create(
BetaContext,
goal=self.context.goal,
classes=self._current_classes(),
file_manager=file_manager,
backlog=backlog,
)
beta_context.runtime_queue_depth_callback = queue_depth_callback
beta_context.test_runner_strategy = self.test_runner_strategy
self._record_queue_depths(self.context, beta=beta_context)
loop = BetaLoop(
gate=MechanicalLoopGate(
context=beta_context,
may_enter=lambda: isinstance(beta_context.backlog, QABacklog),
should_continue=lambda: bool(beta_context.backlog.testing),
should_retry=RetryVibeCheck(
prompt_formatter_builder=beta_context.file_manager.prompt_formatter_builder,
ai_client=beta_context.file_manager.client,
child_name="BetaLoop",
evidence_provider=lambda: beta_context.marginal_result,
),
),
context=beta_context,
aider_worker=self.aider_worker,
frontier_provider=self.reference_frontiers,
previous_cot_records=self.previous_cot_records,
provenance=self.provenance,
logger=self.logger.getChild("beta"),
)
return loop
def _create_alpha_loop(self, bfs_depth: int) -> AlphaLoop:
"""Create the AlphaLoop for one assigned topological frontier."""
file_manager = self.context.file_manager
backlog = self._create_contributor_backlog()
topology_summary = f"Topology {bfs_depth} source backlog pass."
self.new_files_callback = self._root_surrogates_needed_callback
queue_depth_callback = lambda: self._publish_queue_depth_snapshot(alpha=context)
should_thaw = False
if resume_settings().hydrate:
payload = context_factory.latest_payload_for(AlphaContext) or {}
should_thaw = (
payload.get("goal") == self.context.goal
and bool(
payload.get("active_inbound")
or payload.get("outbound_queue")
or payload.get("inbound_queue")
)
)
if should_thaw:
try:
context = context_factory.thaw(
AlphaContext,
goal=self.context.goal,
classes=self._current_classes(),
file_manager=file_manager,
backlog=backlog,
runtime_queue_depth_callback=queue_depth_callback,
)
except FileNotFoundError:
should_thaw = False
if not should_thaw:
context = context_factory.create(
AlphaContext,
goal=self.context.goal,
classes=self._current_classes(),
file_manager=file_manager,
backlog=backlog,
)
context.runtime_queue_depth_callback = queue_depth_callback
context.test_runner_strategy = self.test_runner_strategy
context.new_files_callback = self.new_files_callback
context.topology_summary = topology_summary
self._record_queue_depths(self.context, alpha=context)
loop = AlphaLoop(
gate=MechanicalLoopGate(
context=context,
may_enter=lambda: isinstance(context.backlog, ContributorBacklog),
should_continue=lambda: False,
should_retry=RetryVibeCheck(
prompt_formatter_builder=context.file_manager.prompt_formatter_builder,
ai_client=context.file_manager.client,
child_name="AlphaLoop",
evidence_provider=lambda: context.marginal_result,
),
),
context=context,
aider_worker=self.aider_worker,
frontier_provider=self.reference_frontiers,
previous_cot_root_readonly_file=self.previous_cot_root_readonly_file,
previous_cot_records=self.previous_cot_records,
provenance=self.provenance,
logger=self.logger.getChild("alpha"),
)
return loop
def _summary_hint(self) -> str:
"""Return bounded stabilization evidence from child loop summaries."""
if not self.context.marginal_result:
return f"Stabilization for {self.context.root_change.location} produced no child summaries."
return " ".join(
summary.strip()
for summary in self.context.marginal_result
if summary.strip()
)
def _squash_to_upstream(self, summary: str) -> None:
"""Squash the scoped FileManager into the upstream manager once."""
if self.context.squashed:
return
file_manager = self.context.file_manager
file_manager.squash_to_upstream(summary)
self.context.squashed = True
self.context.persist()
def _prepare_work(self) -> ProtoLoop | None:
"""Return no generic child because Stabilization runs Alpha and Beta explicitly."""
return None
def _schedule_final_exam(self) -> None:
"""Atomically replace drained work with the final full-suite sentinel."""
self.context.backlog.clear_work_queues(logger=self.logger.getChild("backlog"))
self.context.final_exam_started = True
self.context.final_exam_finished = False
self.context.fully_explored = False
marker = ClassMeta(
location="__last_exam__",
summary="last exam",
relevance=["last exam"],
relevant_symbols={},
content_type=ContentType.Test,
)
self.context.backlog.queue_testing(
TestingBacklog(
summary="last exam",
focused_spotlight=marker,
upstream_classes=[],
downstream_classes=[],
change_type=ChangeType.Update,
last_exam=True,
),
logger=self.logger.getChild("backlog"),
)
self.context.persist()
publish_stabilization_status(self.context)
def _current_cot_forward_remediation_locations(self) -> set[str]:
"""Return recursively downstream source locations from the active frontier."""
self._ensure_strategies()
file_manager = self.context.file_manager
assert self.bi_dependency_strategy is not None
frontier, _ = self.context.frontier
current_locations = set(map(lambda source: source.location, frontier))
# The scheduler defers only targets the active frontier can still reach.
downstream_sources = self.bi_dependency_strategy.downstream_sources_for(
frontier,
file_manager.src_classes,
file_manager.src_root,
recursive=True,
)
return set(map(
lambda source: source.location,
downstream_sources,
)) - current_locations
def _defer_forward_remediation(self, forward_locations: set[str]) -> None:
"""Stage only remediation that remains downstream of the active frontier."""
# Final examination starts only after deferred remediation has drained or been handed forward.
if self.context.final_exam_started:
return
frontier_items: list[FailedQAItem] = []
handoff = self.context.remaining_deferred_remediation
deferred_keys = [
RemediationDeduplicationKey.from_item(item)
for item in chain(handoff.inherited_items, handoff.deferred_items)
]
# This is the scheduler gate: only targets still ahead are staged; lineage is not consulted.
for item in self.context.backlog.remediation:
remediation_locations = {
source.location
for source in (item.focused_spotlight, *item.downstream_classes)
if source is not None
}
# Keep sources outside the active frontier's downstream path runnable for Alpha.
if not remediation_locations & forward_locations:
frontier_items.append(item)
continue
key = RemediationDeduplicationKey.from_item(item)
if key in deferred_keys:
continue
deferred_keys.append(key)
location = item.focused_spotlight.location
self.context.deferred_remediation_backlog.setdefault(
location,
[],
).append(item)
self.context.backlog.replace_remediation(frontier_items, logger=self.logger.getChild("backlog"))
def _root_surrogates_needed_callback(
self,
files: dict[str | object, str] | None = None,
**kwargs: str,
) -> bool:
"""Create requested surrogate root sources and queue their direct tests."""
file_manager = self.context.file_manager
if file_manager is None:
return False
requested: dict[object, str] = {}
if files:
requested.update(files)
requested.update(kwargs)
new_class_metas: list[tuple[ClassMeta, str]] = []
for file_name, reason in requested.items():
path = file_manager.source_path(str(file_name))
location = file_manager.source_location(path)
file_manager.repo_exec.mkdir(path.parent, parents=True, exist_ok=True)
if not file_manager.repo_exec.exists(path):
file_manager.repo_exec.write_text(path, "", encoding="utf-8")
new_class_meta = ClassMeta(
location=location,
summary=reason,
relevance=[reason],
relevant_symbols={},
cdc=[(datetime.now(), file_manager.actor, reason)],
content_type=ContentType.Source,
)
new_class_metas.append((new_class_meta, reason))
for new_class_meta, reason in new_class_metas:
file_manager.upsert_src(new_class_meta, reason)
self.context.extend_roots(
*map(lambda item: item[0], new_class_metas)
)
for new_class_meta, reason in new_class_metas:
current_new_class_meta = (
file_manager.current_src(new_class_meta.location)
or new_class_meta
)
upstream_classes = sorted(
self._upstream_sources_for([current_new_class_meta]),
key=lambda item: item.location,
)
for test_file in self._direct_tests_for([current_new_class_meta]):
item = TestingBacklog(
summary=reason,
focused_spotlight=test_file,
upstream_classes=list(upstream_classes),
downstream_classes=[current_new_class_meta],
change_type=ChangeType.Update,
)
self.context.backlog.queue_testing(item)
self.context.persist()
return True
def _promote_deferred_remediation(self, forward_locations: set[str]) -> None:
"""Promote deferred remediation once no required target remains forward."""
queued_keys = [
RemediationDeduplicationKey.from_item(item)
for item in self.context.backlog.remediation
]
# Promotion may consume either carryover or local deferral but never merges their storage.
for backlog in (
self.context.inherited_remediation_backlog,
self.context.deferred_remediation_backlog,
):
# Reconcile each source owner independently inside its original staging lane.
for location in list(backlog):
items = backlog[location]
remaining = []
# Promote exactly when the active frontier no longer reaches it downstream.
for item in items:
remediation_locations = {
source.location
for source in (item.focused_spotlight, *item.downstream_classes)
if source is not None
}
# Retain only remediation whose source remains forward of the active frontier.
if remediation_locations & forward_locations:
remaining.append(item)
continue
key = RemediationDeduplicationKey.from_item(item)
if key in queued_keys:
continue
queued_keys.append(key)
self.context.backlog.append_existing_remediation(item, logger=self.logger.getChild("backlog"))
if remaining:
backlog[location] = remaining
else:
del backlog[location]
def _remediation_relations_for_reporting(
self,
focus_locations: set[str],
) -> dict[str, str]:
"""Describe supplied focus locations' root connectivity through the BiGraph."""
self._ensure_strategies()
file_manager = self.context.file_manager
assert self.bi_dependency_strategy is not None
# Root-recursive connectivity is diagnostic only; queue routing receives
# an explicit active-frontier recursive downstream set from its caller.
downstream = {
source.location
for source in self.bi_dependency_strategy.downstream_sources_for(
self.context.t0,
file_manager.src_classes,
file_manager.src_root,
recursive=True,
)
}
upstream = {
source.location
for source in self.bi_dependency_strategy.upstream_sources_for(
self.context.t0,
file_manager.src_classes,
file_manager.src_root,
file_manager.test_root,
recursive=True,
)
}
return {
**dict.fromkeys(focus_locations - downstream - upstream, "neither"),
**dict.fromkeys(
(focus_locations & downstream) - upstream,
"downstream",
),
**dict.fromkeys(
(focus_locations & upstream) - downstream,
"upstream",
),
**dict.fromkeys(focus_locations & downstream & upstream, "both"),
}
def _classify_remediation_for_reporting(
self,
item: FailedQAItem,
forward_locations: set[str],
relation: str,
) -> RemediationClassificationResult:
"""Classify remediation for logs without changing queue routing or ownership."""
focus_location = getattr(item.focused_spotlight, "location", "[missing focus]")
admission_location_set = {
source.location
for source in (item.focused_spotlight, *item.downstream_classes)
if source is not None
}
admission_locations = tuple(sorted(admission_location_set))
covered_locations = {
source.location
for frontier in self.context.full_topologies
for source in frontier
}
if admission_location_set <= covered_locations:
return RemediationClassificationResult(
classification=RemediationClassification.NORMAL,
focus_location=focus_location,
admission_locations=admission_locations,
relation=relation,
reason="all remediation targets are already covered by the current COT",
)
if admission_location_set & forward_locations:
return RemediationClassificationResult(
classification=RemediationClassification.DEFERRED,
focus_location=focus_location,
admission_locations=admission_locations,
relation=relation,
reason="at least one remediation target remains forward in the current COT",
)
history = self.context.prior_traversed_frontiers
previous = (
next(filter(None, map(history.find_location, admission_locations)), None)
if history is not None
else None
)
if previous is not None:
return RemediationClassificationResult(
classification=RemediationClassification.WALK_BACK,
focus_location=focus_location,
admission_locations=admission_locations,
previous_cot_index=previous[0],
previous_topology_index=previous[1],
relation=relation,
reason="a remediation target was traversed by a prior COT",
)
return RemediationClassificationResult(
classification=RemediationClassification.LATENT_HIDDEN_BUG,
focus_location=focus_location,
admission_locations=admission_locations,
relation=relation,
reason="no remediation target is forward in this COT or present in prior lineage",
)
def _log_remediation_classification(
self,
result: RemediationClassificationResult,
) -> None:
"""Emit one structured, non-routing remediation classification log."""
levels = {
RemediationClassification.NORMAL: logging.INFO,
RemediationClassification.DEFERRED: logging.INFO,
RemediationClassification.WALK_BACK: logging.WARNING,
RemediationClassification.LATENT_HIDDEN_BUG: logging.ERROR,
}
classification_fields = {
RemediationClassification.NORMAL: ("reached_current=true",),
RemediationClassification.DEFERRED: (
"downstream_current=true",
"reached_current=false",
),
RemediationClassification.WALK_BACK: (f"relation={result.relation}",),
RemediationClassification.LATENT_HIDDEN_BUG: (
f"relation={result.relation}",
),
}
prior_fields = (
(
f"prior_cot={result.previous_cot_index}",
f"prior_topology={result.previous_topology_index}",
)
if result.previous_cot_index is not None
else ()
)
fields = (
f"focus={result.focus_location}",
f"cot={self.context.cot_index}",
*classification_fields[result.classification],
*prior_fields,
)
self.logger.log(
levels[result.classification],
"Remediation readiness %s %s. %s",
result.classification.name,
" ".join(fields),
result.reason,
)
def _report_remediation_classifications(
self,
forward_locations: set[str],
) -> None:
"""Report every live remediation lane without mutating scheduler state."""
handoff = self.context.remaining_deferred_remediation
items_by_id = {
id(item): item
for item in chain(
self.context.backlog.remediation,
handoff.inherited_items,
handoff.deferred_items,
)
}
relations = self._remediation_relations_for_reporting(
{
item.focused_spotlight.location
for item in items_by_id.values()
if item.focused_spotlight is not None
}
)
for item in items_by_id.values():
self._log_remediation_classification(
self._classify_remediation_for_reporting(
item,
forward_locations,
relations.get(
getattr(item.focused_spotlight, "location", "[missing focus]"),
"neither",
),
)
)
def _run_testing_pass(self) -> None:
"""Run Beta over the durable testing backlog."""
beta = self._create_beta_loop()
super()._run(beta)
self._record_queue_depths(self.context)
self.context.persist()
def run(self, logger_name: str | None = None) -> str:
"""Run source and test sibling phases until stabilization stops, expands, or blocks."""
# Establish the stabilization logger before any child phase can publish status.
if not self._logger_stack:
self._logger_stack.append(llm_logging.logger_for_loop(self))
# Scope an optional caller label to this run and restore it at the owning boundary.
if logger_name:
self._logger_stack.append(
self.logger.getChild(llm_logging.safe_log_segment(logger_name))
)
llm_logging.set_current_logger(self.logger)
# Keep logger restoration paired with every scheduling or terminal exit.
try:
publish_loop_status(
current_loop=type(self).__name__,
current_item=self.context.root_change.location,
)
publish_stabilization_status(self.context)
# Respect the outer gate before mutating frontier or backlog state.
if not self.gate.may_enter():
return self._post_run()
frontier, bfs_depth = self.context.frontier
# The active depth keeps its original pre-Alpha baseline across persistence and resume.
if bfs_depth not in self.context.frontier_open_hashes:
self.context.frontier_open_hashes[bfs_depth] = (
self._current_frontier_hashes()
)
# Bootstrap source work is durable before topology may service depth zero.
if bfs_depth == 0:
self._enqueue_current_frontier_to_backlog()
# Repeat worker service or one topology cadence until a terminal fact is proven.
while True:
publish_stabilization_status(self.context)
handoff = self.context.remaining_deferred_remediation
# Reconcile deferred remediation before selecting the next runnable phase.
if (
self.context.backlog.remediation
or handoff.inherited_items
or handoff.deferred_items
):
# Keep only still-forward remediation staged; outside-trajectory sources stay active.
forward_locations = self._current_cot_forward_remediation_locations()
self._promote_deferred_remediation(forward_locations)
self._defer_forward_remediation(forward_locations)
self.context.persist()
self._report_remediation_classifications(forward_locations)
# Let Alpha drain source-side work after the existing forward-expansion opportunity.
if self.context.backlog.remediation or self.context.backlog.inspection:
# Real control flow beyond backlog shaping: if any source-side backlog remains,
# let Alpha drain it. Alpha/backlog chooses remediation before inspection.
_, bfs_depth = self.context.frontier
self.context.active_topology = bfs_depth
self.context.persist()
publish_loop_status(
previous_loop=type(self).__name__,
current_loop="AlphaLoop",
next_loop="BetaLoop",
current_item=f"Alpha starting: topology {bfs_depth}",
)
publish_stabilization_status(self.context)
alpha = self._create_alpha_loop(bfs_depth)
super()._run(alpha)
self._record_queue_depths(self.context)
self.context.active_topology = None
self.context.persist()
publish_stabilization_status(self.context)
# Real control flow beyond backlog shaping: if source-side backlog is clear,
# let Beta drain queued testing work next.
elif self.context.backlog.testing:
publish_loop_status(
previous_loop=type(self).__name__,
current_loop="BetaLoop",
current_item="Beta starting",
)
self._run_testing_pass()
publish_stabilization_status(self.context)
# Stop child scheduling once the only remaining work belongs to the parent transition.
elif self.context.accepted:
break
else:
frontier, bfs_depth = self.context.frontier
self._grow_deferred_frontier_whitelist_locations(frontier)
opening_hashes = self.context.frontier_open_hashes.get(
bfs_depth
)
# Fresh in-memory state captures once; supported checkpoints already carry it.
if opening_hashes is None:
opening_hashes = self._current_frontier_hashes()
self.context.frontier_open_hashes[bfs_depth] = opening_hashes
boundary = self._frontier_boundary(
frontier,
opening_hashes,
)
ready_frontier = self._topology_cadence(boundary)
# The local raw pull finishes before the topology poll returns.
remaining_downstream = False
backlog = self.context.backlog
runnable_work = bool(
backlog.inspection
or backlog.remediation
or backlog.testing
)
deferred_topology = self.context.topology.has_deferred_frontier
handoff = self.context.remaining_deferred_remediation
future_owned = self.context.topology.future_cot_owned_locations
deferred_remediation = (
*handoff.inherited_items,
*handoff.deferred_items,
)
remediation_locations = set(map(
lambda source: source.location,
filter(
lambda source: source is not None,
chain.from_iterable(map(
lambda item: (
item.focused_spotlight,
*item.downstream_classes,
),
deferred_remediation,
)),
),
))
has_unlocated_remediation = any(map(
lambda item: not any(filter(
lambda source: source is not None,
(item.focused_spotlight, *item.downstream_classes),
)),
deferred_remediation,
))
# Only an explicitly future-owned remediation item leaves this COT's terminal algebra.
current_owned_remediation = bool(
remediation_locations - future_owned
) or has_unlocated_remediation
# Admission and Alpha durability form one transition before topology advances.
if ready_frontier:
self.context.append_frontier(ready_frontier)
_, admitted_depth = self.context.frontier
self.context.frontier_open_hashes[admitted_depth] = (
self._current_frontier_hashes()
)
self._enqueue_current_frontier_to_backlog()
publish_stabilization_status(self.context)
continue
# A completed reconciliation may persist; an earlier pop never persists alone.
self.context.persist()
quiescent = (
not ready_frontier
and not remaining_downstream
and not runnable_work
)
# Another staged boundary owns the next outer iteration.
if not quiescent:
continue
# Only current-owned topology or remediation deferral can block quiescence.
if deferred_topology or current_owned_remediation:
# Emit the bounded topology snapshot only when topology owns a blocker.
if deferred_topology:
self._log_deferred_topology(boundary)
self.context.blocked_summary = (
"Story-local backlog remains unresolved."
)
self.context.persist()
break
last_cot = self.context.cot_index == self.context.total_cot - 1
# The final COT must schedule its full-suite sentinel before acceptance.
if last_cot and not self.context.final_exam_started:
self._schedule_final_exam()
continue
# A drained final-exam cadence closes the final COT.
if last_cot:
self.context.final_exam_finished = True
self.context.fully_explored = True
self.context.accepted = True
self.context.persist()
self.logger.info(
"Stabilization accepted within current COT ownership; story completion remains pending."
)
publish_stabilization_status(self.context)
break
return self._post_run()
finally:
# Remove only the optional caller label installed by this invocation.
if logger_name:
self._logger_stack.pop()
llm_logging.set_current_logger(self.logger if self._logger_stack else None)
def _post_run(self) -> str:
"""Return the stabilization summary and squash scoped evidence only after acceptance."""
summary_hint = self._summary_hint()
if not self.context.accepted:
summary_hint = (
f"{summary_hint} Stabilization incomplete; upstream squash was not performed."
)
if self.context.blocked_summary:
summary_hint = f"{summary_hint} {self.context.blocked_summary}"
if self.context.accepted:
summary_hint = (
f"{summary_hint} Stabilization accepted within current COT ownership."
)
file_manager = self.context.file_manager
summary = file_manager.summarize(summary_hint)
if self.context.accepted:
self._squash_to_upstream(summary)
return f"Stabilization for {self.context.root_change.location}: {summary}"
from band_aider.ai_client.json_completion import load_json_completion