1279 lines
53 KiB
Python
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
|