misterdev 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- misterdev/__init__.py +3 -0
- misterdev/agent.py +2166 -0
- misterdev/agent_helpers.py +194 -0
- misterdev/analyzers/__init__.py +0 -0
- misterdev/analyzers/project_analyzer/__init__.py +246 -0
- misterdev/analyzers/project_analyzer/detection.py +146 -0
- misterdev/analyzers/project_analyzer/merge.py +137 -0
- misterdev/analyzers/project_analyzer/overview.py +312 -0
- misterdev/analyzers/project_analyzer/prompts.py +83 -0
- misterdev/cli.py +370 -0
- misterdev/config.py +521 -0
- misterdev/core/__init__.py +0 -0
- misterdev/core/audit.py +88 -0
- misterdev/core/config.py +10 -0
- misterdev/core/context/__init__.py +0 -0
- misterdev/core/context/change_tracker.py +218 -0
- misterdev/core/context/contracts/__init__.py +63 -0
- misterdev/core/context/contracts/_log.py +9 -0
- misterdev/core/context/contracts/_text.py +12 -0
- misterdev/core/context/contracts/extraction.py +30 -0
- misterdev/core/context/contracts/python_generic.py +42 -0
- misterdev/core/context/contracts/registry.py +127 -0
- misterdev/core/context/contracts/rust_line.py +225 -0
- misterdev/core/context/contracts/rust_tree_sitter.py +141 -0
- misterdev/core/context/lsp.py +174 -0
- misterdev/core/context/scratchpad.py +111 -0
- misterdev/core/context/topography/__init__.py +43 -0
- misterdev/core/context/topography/_log.py +10 -0
- misterdev/core/context/topography/cache.py +91 -0
- misterdev/core/context/topography/engine.py +172 -0
- misterdev/core/context/topography/graph.py +675 -0
- misterdev/core/context/topography/nodes.py +55 -0
- misterdev/core/context/topography/parsers.py +95 -0
- misterdev/core/context/topography/syntax.py +54 -0
- misterdev/core/economics/__init__.py +0 -0
- misterdev/core/economics/context_budget.py +175 -0
- misterdev/core/economics/embeddings.py +232 -0
- misterdev/core/economics/free_models.py +108 -0
- misterdev/core/economics/llm_cache.py +105 -0
- misterdev/core/economics/model_catalog.py +79 -0
- misterdev/core/economics/model_ledger.py +331 -0
- misterdev/core/economics/model_selector.py +281 -0
- misterdev/core/execution/__init__.py +0 -0
- misterdev/core/execution/bounded.py +50 -0
- misterdev/core/execution/container.py +221 -0
- misterdev/core/execution/error_classifier.py +366 -0
- misterdev/core/execution/error_resolver.py +201 -0
- misterdev/core/execution/governance.py +283 -0
- misterdev/core/execution/outcomes.py +50 -0
- misterdev/core/execution/progress.py +120 -0
- misterdev/core/execution/project.py +231 -0
- misterdev/core/execution/registry.py +97 -0
- misterdev/core/execution/runtime.py +279 -0
- misterdev/core/gitcmd.py +39 -0
- misterdev/core/integration/__init__.py +0 -0
- misterdev/core/integration/mcp.py +368 -0
- misterdev/core/integration/mcp_gather.py +186 -0
- misterdev/core/models.py +35 -0
- misterdev/core/modes.py +184 -0
- misterdev/core/planning/__init__.py +0 -0
- misterdev/core/planning/advisor.py +89 -0
- misterdev/core/planning/assessment.py +135 -0
- misterdev/core/planning/decomposer.py +387 -0
- misterdev/core/planning/metacognition.py +103 -0
- misterdev/core/planning/sovereign.py +308 -0
- misterdev/core/planning/targets.py +201 -0
- misterdev/core/reporting/__init__.py +0 -0
- misterdev/core/reporting/report.py +377 -0
- misterdev/core/reporting/report_view.py +151 -0
- misterdev/core/task.py +163 -0
- misterdev/core/verification/__init__.py +0 -0
- misterdev/core/verification/claim_verifier.py +210 -0
- misterdev/core/verification/critic.py +324 -0
- misterdev/core/verification/gatekeeper/__init__.py +631 -0
- misterdev/core/verification/gatekeeper/constants.py +138 -0
- misterdev/core/verification/gatekeeper/helpers.py +28 -0
- misterdev/core/verification/goal_check.py +219 -0
- misterdev/core/verification/independent.py +68 -0
- misterdev/core/verification/mutation_gate.py +221 -0
- misterdev/core/verification/preflight.py +95 -0
- misterdev/core/verification/spec_tests.py +175 -0
- misterdev/core/verification/validator.py +495 -0
- misterdev/core/verification/vision_verify.py +185 -0
- misterdev/core/verification/web_verify.py +408 -0
- misterdev/environments/__init__.py +0 -0
- misterdev/environments/base_env.py +18 -0
- misterdev/environments/container_env.py +87 -0
- misterdev/environments/venv_env.py +42 -0
- misterdev/llm/__init__.py +0 -0
- misterdev/llm/client/__init__.py +152 -0
- misterdev/llm/client/base.py +382 -0
- misterdev/llm/client/edits.py +70 -0
- misterdev/llm/client/embeddings.py +121 -0
- misterdev/llm/client/errors.py +134 -0
- misterdev/llm/client/providers.py +535 -0
- misterdev/llm/client/response.py +24 -0
- misterdev/llm/prompt_manager.py +82 -0
- misterdev/llm/responses/__init__.py +34 -0
- misterdev/llm/responses/apply.py +131 -0
- misterdev/llm/responses/json_extract.py +80 -0
- misterdev/llm/responses/models.py +43 -0
- misterdev/llm/responses/parsing.py +494 -0
- misterdev/logging_setup.py +20 -0
- misterdev/mcp_server.py +208 -0
- misterdev/nl_cli.py +149 -0
- misterdev/plugins.py +115 -0
- misterdev/py.typed +0 -0
- misterdev/task_executors/__init__.py +0 -0
- misterdev/task_executors/base_executor.py +10 -0
- misterdev/task_executors/markdown_plan_executor/__init__.py +90 -0
- misterdev/task_executors/markdown_plan_executor/commands_mixin.py +82 -0
- misterdev/task_executors/markdown_plan_executor/context_mixin.py +221 -0
- misterdev/task_executors/markdown_plan_executor/critic_spec_mixin.py +174 -0
- misterdev/task_executors/markdown_plan_executor/edits_mixin.py +251 -0
- misterdev/task_executors/markdown_plan_executor/execute_mixin.py +727 -0
- misterdev/task_executors/markdown_plan_executor/gates_mixin.py +203 -0
- misterdev/task_executors/markdown_plan_executor/git_mixin.py +219 -0
- misterdev/task_executors/markdown_plan_executor/helpers.py +521 -0
- misterdev/task_executors/markdown_plan_executor/llm_mixin.py +238 -0
- misterdev/task_executors/markdown_plan_executor/results_mixin.py +23 -0
- misterdev/tools/__init__.py +19 -0
- misterdev/tools/base_tool.py +14 -0
- misterdev/tools/command.py +75 -0
- misterdev/tools/file_io.py +69 -0
- misterdev/tools/formatter.py +26 -0
- misterdev/tools/git_tool.py +90 -0
- misterdev/utils/__init__.py +0 -0
- misterdev/utils/file_utils.py +169 -0
- misterdev/utils/process.py +23 -0
- misterdev-0.2.0.dist-info/METADATA +326 -0
- misterdev-0.2.0.dist-info/RECORD +136 -0
- misterdev-0.2.0.dist-info/WHEEL +5 -0
- misterdev-0.2.0.dist-info/entry_points.txt +3 -0
- misterdev-0.2.0.dist-info/licenses/COMMERCIAL_LICENSE.md +34 -0
- misterdev-0.2.0.dist-info/licenses/LICENSE +661 -0
- misterdev-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
"""The Try-Test-Fix execution loop (``execute``)."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from misterdev.core.economics.context_budget import ContextBudget
|
|
6
|
+
from misterdev.core.models import Task, ExecutionResult
|
|
7
|
+
from misterdev.core.execution.project import Project
|
|
8
|
+
from misterdev.core.verification.validator import (
|
|
9
|
+
CodeValidator,
|
|
10
|
+
CertaintyScorer,
|
|
11
|
+
)
|
|
12
|
+
from misterdev.core.execution.error_resolver import ErrorResolver
|
|
13
|
+
from misterdev.core.execution.error_classifier import (
|
|
14
|
+
format_classified_error,
|
|
15
|
+
)
|
|
16
|
+
from misterdev.llm.client import CACHE_BREAKPOINT
|
|
17
|
+
from misterdev.llm.prompt_manager import PromptManager
|
|
18
|
+
from misterdev.config import get_setting
|
|
19
|
+
|
|
20
|
+
from .helpers import (
|
|
21
|
+
logger,
|
|
22
|
+
_is_golden_path,
|
|
23
|
+
_detect_language,
|
|
24
|
+
EDIT_FORMAT_INSTRUCTIONS,
|
|
25
|
+
FULL_FILE_FALLBACK_INSTRUCTIONS,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ExecuteMixin:
|
|
30
|
+
def execute(
|
|
31
|
+
self, task: Task, project: Project, use_git_branch: bool = True, _depth: int = 0
|
|
32
|
+
) -> ExecutionResult:
|
|
33
|
+
logger.info(f"Starting execution of task: {task.id}")
|
|
34
|
+
project.task_manager.update_task_status(task.id, "in_progress")
|
|
35
|
+
|
|
36
|
+
prompt_manager = PromptManager(project.config)
|
|
37
|
+
processor_config = self._get_processor_config(project)
|
|
38
|
+
|
|
39
|
+
# Read max_task_attempts from orchestrator config, falling back to processor config, then hardcoded default
|
|
40
|
+
default_attempts = get_setting(
|
|
41
|
+
project.config, "orchestrator", "max_task_attempts"
|
|
42
|
+
)
|
|
43
|
+
max_retries = processor_config.get("max_retries_per_task", default_attempts)
|
|
44
|
+
|
|
45
|
+
# Read context budget tokens from orchestrator config or task processor_data
|
|
46
|
+
context_budget_tokens = task.processor_data.get(
|
|
47
|
+
"context_budget_tokens",
|
|
48
|
+
get_setting(project.config, "orchestrator", "context_budget_tokens"),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Minimum LLM certainty required to accept a task as completed when no
|
|
52
|
+
# test gate ran. Deterministic: compared against the heuristic certainty
|
|
53
|
+
# score only, never another LLM call.
|
|
54
|
+
certainty_threshold = get_setting(
|
|
55
|
+
project.config, "orchestrator", "certainty_threshold"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Per-task acceptance gate. Cheap deterministic command path is on by
|
|
59
|
+
# default; the LLM-judge fallback is off by default so the default path
|
|
60
|
+
# adds zero extra LLM calls. When no command is found and the judge is
|
|
61
|
+
# off, acceptance is a no-op (behaviour identical to before this gate).
|
|
62
|
+
verify_acceptance = get_setting(
|
|
63
|
+
project.config, "orchestrator", "verify_acceptance"
|
|
64
|
+
)
|
|
65
|
+
llm_acceptance_judge = get_setting(
|
|
66
|
+
project.config, "orchestrator", "llm_acceptance_judge"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
files_key = processor_config.get("target_files_key", "files_to_modify")
|
|
70
|
+
test_cmd_key = processor_config.get("test_command_key", "test_command")
|
|
71
|
+
typecheck_cmd_key = processor_config.get(
|
|
72
|
+
"typecheck_command_key", "typecheck_command"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
target_files = task.processor_data.get(files_key, [])
|
|
76
|
+
if isinstance(target_files, str):
|
|
77
|
+
target_files = [target_files]
|
|
78
|
+
target_files = list(
|
|
79
|
+
set(target_files + task.files_to_modify + task.files_to_create)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Golden suite: never task the model with these files and never read
|
|
83
|
+
# them into its context. Combined with the edit-time rejection in
|
|
84
|
+
# _validate_edit_paths, the model cannot see or alter them.
|
|
85
|
+
golden_paths = get_setting(project.config, "orchestrator", "golden_paths")
|
|
86
|
+
target_files = [f for f in target_files if not _is_golden_path(f, golden_paths)]
|
|
87
|
+
context_files = [
|
|
88
|
+
f for f in task.context_files if not _is_golden_path(f, golden_paths)
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
# Multi-target routing: pick the sub-project that owns this task's files
|
|
92
|
+
# and gate with ITS build/test/typecheck commands. No targets / no match
|
|
93
|
+
# -> top-level commands, i.e. the single-target path is unchanged.
|
|
94
|
+
from misterdev.core.planning.targets import (
|
|
95
|
+
select_target,
|
|
96
|
+
target_commands,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
routed_target = select_target(project.config.get("targets") or [], target_files)
|
|
100
|
+
target_cmds = target_commands(routed_target, project.config)
|
|
101
|
+
# Routed gates run in the TARGET's directory (so `npm run typecheck`
|
|
102
|
+
# resolves under clients/web, not the repo root). None -> project.path.
|
|
103
|
+
task_cwd = None
|
|
104
|
+
if routed_target is not None:
|
|
105
|
+
tp = (routed_target.get("path") or "").strip("/")
|
|
106
|
+
task_cwd = project.path / tp if tp else project.path
|
|
107
|
+
logger.info(
|
|
108
|
+
f"Task routed to target "
|
|
109
|
+
f"'{routed_target.get('name') or routed_target.get('path')}' "
|
|
110
|
+
f"(cwd={tp or '.'}): build={target_cmds['build_command']!r}, "
|
|
111
|
+
f"test={target_cmds['test_command']!r}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
test_command = task.processor_data.get(test_cmd_key)
|
|
115
|
+
if routed_target is not None and target_cmds["test_command"] is not None:
|
|
116
|
+
test_command = target_cmds["test_command"]
|
|
117
|
+
typecheck_command = (
|
|
118
|
+
task.processor_data.get(typecheck_cmd_key)
|
|
119
|
+
or target_cmds["typecheck_command"]
|
|
120
|
+
)
|
|
121
|
+
task_build_command = target_cmds["build_command"]
|
|
122
|
+
build_timeout = get_setting(project.config, "build", "build_timeout")
|
|
123
|
+
test_timeout = get_setting(project.config, "build", "test_timeout")
|
|
124
|
+
|
|
125
|
+
# Atomic execution: git branch per task (disabled in parallel mode to avoid races)
|
|
126
|
+
can_branch = use_git_branch and self._is_git_repo(project)
|
|
127
|
+
branch_name = f"task/{task.id}" if can_branch else None
|
|
128
|
+
base_branch = None
|
|
129
|
+
|
|
130
|
+
if branch_name:
|
|
131
|
+
base_branch = self._get_current_branch(project)
|
|
132
|
+
if not self._create_task_branch(project, branch_name):
|
|
133
|
+
logger.warning(
|
|
134
|
+
"Failed to create task branch; falling back to file snapshots"
|
|
135
|
+
)
|
|
136
|
+
branch_name = None
|
|
137
|
+
|
|
138
|
+
# Untracked files present BEFORE the task, so revert can clean only the
|
|
139
|
+
# orphans this task creates without touching pre-existing untracked work.
|
|
140
|
+
untracked_before = (
|
|
141
|
+
self._untracked_files(project) if self._is_git_repo(project) else set()
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Fallback: file snapshots if git branching isn't available
|
|
145
|
+
snapshot = None
|
|
146
|
+
if not branch_name:
|
|
147
|
+
snapshot = self._snapshot_files(project, target_files)
|
|
148
|
+
|
|
149
|
+
# Seed the first attempt with the baseline test failures so a fix task
|
|
150
|
+
# debugs against the REAL errors from the start, not blindly. Empty on a
|
|
151
|
+
# green baseline -> error_logs stays None and attempt 0 uses the normal
|
|
152
|
+
# task template (unchanged behavior).
|
|
153
|
+
seed_output = getattr(project, "baseline_test_output", "") or ""
|
|
154
|
+
error_logs = (
|
|
155
|
+
f"The project's test suite is currently failing. Fix the failures "
|
|
156
|
+
f"below (focus on this task's files):\n{seed_output[:4000]}"
|
|
157
|
+
if seed_output.strip()
|
|
158
|
+
else None
|
|
159
|
+
)
|
|
160
|
+
prior_errors: List[str] = []
|
|
161
|
+
# Count anchored-edit application failures so we can fall back to a
|
|
162
|
+
# full-file rewrite when SEARCH/REPLACE keeps not matching (a stall that
|
|
163
|
+
# otherwise makes no progress across attempts).
|
|
164
|
+
apply_failures = 0
|
|
165
|
+
# Build the symbol graph now (idempotent, lazy): it isn't constructed at
|
|
166
|
+
# project registration anymore, and task execution is the first consumer.
|
|
167
|
+
try:
|
|
168
|
+
project.topography.initialize()
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.warning(f"Topography init failed (continuing without graph): {e}")
|
|
171
|
+
resolver = ErrorResolver(project.path, project.topography.graph)
|
|
172
|
+
# Every in-root path the LLM actually wrote, across all attempts. Commit
|
|
173
|
+
# exactly these (plus declared targets) so an out-of-scope-but-valid
|
|
174
|
+
# edit isn't applied-then-orphaned by staging only the declared files.
|
|
175
|
+
edited_files: set = set()
|
|
176
|
+
|
|
177
|
+
# Optional adversarial critic (independent second component): reviews each
|
|
178
|
+
# candidate edit before it is applied and can force a regeneration with
|
|
179
|
+
# concrete objections. Off by default. Bounded so it defers to the real
|
|
180
|
+
# gates after a few rejections rather than starving the attempt loop.
|
|
181
|
+
critic_enabled = self._critic_enabled_for(project, task)
|
|
182
|
+
critic_max_rejections = get_setting(
|
|
183
|
+
project.config, "orchestrator", "critic_max_rejections"
|
|
184
|
+
)
|
|
185
|
+
critic_rejections = 0
|
|
186
|
+
|
|
187
|
+
# Spec-as-tests (opt-in): generate a failing test from the acceptance
|
|
188
|
+
# criteria BEFORE implementation, written under .orchestrator/spec_tests/
|
|
189
|
+
# (outside the project suite, so it never flips the integration-gate
|
|
190
|
+
# baseline). After the task's gates pass, it is run scoped and must now
|
|
191
|
+
# pass (red -> green). Advisory unless spec_as_tests_block.
|
|
192
|
+
spec_test_path = self._maybe_generate_spec_test(project, task)
|
|
193
|
+
spec_test_block = get_setting(
|
|
194
|
+
project.config, "orchestrator", "spec_as_tests_block"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# The most recent attempt awaiting an outcome. Recorded as a failure at
|
|
198
|
+
# the top of the next iteration (we only loop again when an attempt
|
|
199
|
+
# failed) and at loop exit; recorded as a success at the success seams.
|
|
200
|
+
pending_attempt: Optional[dict] = None
|
|
201
|
+
# Baseline full-suite failure count (from analysis), so a RED baseline
|
|
202
|
+
# doesn't reject every task. The test gate then accepts an attempt that
|
|
203
|
+
# leaves the suite no worse (failures <= baseline), letting a multi-failure
|
|
204
|
+
# project be reduced incrementally instead of demanding one task fix the
|
|
205
|
+
# whole suite. 0 (a green baseline) keeps the gate strictly green-only.
|
|
206
|
+
baseline_failures = int(getattr(project, "baseline_test_failures", 0) or 0)
|
|
207
|
+
if routed_target is not None:
|
|
208
|
+
# Never apply the top-level (e.g. core) baseline to a DIFFERENT
|
|
209
|
+
# target's gate. Use that target's own measured baseline if present,
|
|
210
|
+
# else 0 (strict green-only) — far safer than inheriting core's count.
|
|
211
|
+
per_target = getattr(project, "target_baselines", None) or {}
|
|
212
|
+
key = routed_target.get("name") or routed_target.get("path")
|
|
213
|
+
baseline_failures = int(per_target.get(key, 0) or 0)
|
|
214
|
+
_selector = getattr(project, "model_selector", None)
|
|
215
|
+
track_models = (
|
|
216
|
+
_selector is not None
|
|
217
|
+
and _selector.enabled
|
|
218
|
+
and hasattr(project.llm_client, "task_cost")
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Whole-project structural map (every file + its symbols). Computed once
|
|
222
|
+
# — it is identical across attempts — so the model edits with the entire
|
|
223
|
+
# project's shape in view, not just the target files. ContextBudget
|
|
224
|
+
# trims it first (priority 3) when space is tight.
|
|
225
|
+
topo = getattr(project, "topography", None)
|
|
226
|
+
project_outline = topo.get_project_outline() if topo is not None else ""
|
|
227
|
+
|
|
228
|
+
# Optional, off-by-default agentic pre-edit gathering: when enabled and
|
|
229
|
+
# an MCP manager with tools exists, the model may request bounded MCP
|
|
230
|
+
# tool calls to gather information. The result is prepended to the task
|
|
231
|
+
# context; off → "" and the path below is byte-identical to today.
|
|
232
|
+
mcp_gathered = self._mcp_gather(project, task)
|
|
233
|
+
|
|
234
|
+
for attempt in range(max_retries):
|
|
235
|
+
logger.info(f"Attempt {attempt + 1}/{max_retries} for task {task.id}")
|
|
236
|
+
if pending_attempt is not None:
|
|
237
|
+
self._ledger_record(project, task, pending_attempt, success=False)
|
|
238
|
+
pending_attempt = None
|
|
239
|
+
|
|
240
|
+
code_context = self._get_code_context(
|
|
241
|
+
project, target_files, context_files, task=task
|
|
242
|
+
)
|
|
243
|
+
topo_context = project.topography.get_context_for_task(
|
|
244
|
+
task.description,
|
|
245
|
+
target_files,
|
|
246
|
+
ranker=getattr(project, "semantic_ranker", None),
|
|
247
|
+
exclude_files=self._fully_shown_target_files(project, target_files),
|
|
248
|
+
)
|
|
249
|
+
# Exhaustive external references to the symbols being edited, so a
|
|
250
|
+
# delete/rename/refactor updates every call site in one attempt
|
|
251
|
+
# instead of chasing them one build-error at a time (the dominant
|
|
252
|
+
# failure mode: missing_symbol/wrong_type across attempts).
|
|
253
|
+
reference_sites = project.topography.reference_sites(target_files)
|
|
254
|
+
scratchpad_context = self.scratchpad.format_context(
|
|
255
|
+
files=target_files + context_files,
|
|
256
|
+
tags=[task.category],
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
strategy = task.processor_data.get("strategy", "iterative")
|
|
260
|
+
consensus = task.processor_data.get("consensus_context", "None")
|
|
261
|
+
interface_contracts = task.processor_data.get("interface_contracts", "")
|
|
262
|
+
recent_changes = task.processor_data.get("recent_changes", "")
|
|
263
|
+
|
|
264
|
+
# Budget-aware context allocation using configured token limit
|
|
265
|
+
budget = ContextBudget(max_tokens=context_budget_tokens)
|
|
266
|
+
budget.set("code_context", code_context, priority=1)
|
|
267
|
+
# Correctness-critical: the complete reference set must survive
|
|
268
|
+
# truncation, or a rename/delete misses sites and the build fails.
|
|
269
|
+
budget.set("reference_sites", reference_sites, priority=1, min_lines=0)
|
|
270
|
+
# topo_context is the task-ranked (relevant-first) symbol-graph map
|
|
271
|
+
# that cross-file tasks (delete-all-refs, wire call-sites) need. At
|
|
272
|
+
# priority=3/min_lines=10 it collapsed to ~10 lines on a large repo
|
|
273
|
+
# (the emathy run: "kept 11/9094"), editing blind. Keep it above the
|
|
274
|
+
# truncate-first tier with a real floor so the top-ranked symbols
|
|
275
|
+
# survive; truncation still trims the long tail under pressure.
|
|
276
|
+
budget.set("topo_context", topo_context, priority=2, min_lines=40)
|
|
277
|
+
budget.set("error_logs", error_logs or "", priority=1, min_lines=20)
|
|
278
|
+
budget.set("scratchpad", scratchpad_context, priority=3)
|
|
279
|
+
budget.set("interface_contracts", interface_contracts, priority=2)
|
|
280
|
+
budget.set("recent_changes", recent_changes, priority=2)
|
|
281
|
+
budget.set("consensus_context", consensus, priority=3)
|
|
282
|
+
budget.set("project_outline", project_outline, priority=3, min_lines=0)
|
|
283
|
+
allocated = budget.allocate()
|
|
284
|
+
|
|
285
|
+
full_code_context = allocated["code_context"]
|
|
286
|
+
if allocated["reference_sites"]:
|
|
287
|
+
full_code_context += "\n\n" + allocated["reference_sites"]
|
|
288
|
+
if allocated["topo_context"]:
|
|
289
|
+
full_code_context += "\n\n" + allocated["topo_context"]
|
|
290
|
+
if allocated["recent_changes"]:
|
|
291
|
+
full_code_context += "\n\n" + allocated["recent_changes"]
|
|
292
|
+
if allocated["project_outline"]:
|
|
293
|
+
full_code_context += (
|
|
294
|
+
"\n\n## Project Structure (files and their symbols)\n"
|
|
295
|
+
+ allocated["project_outline"]
|
|
296
|
+
)
|
|
297
|
+
if mcp_gathered:
|
|
298
|
+
full_code_context += mcp_gathered
|
|
299
|
+
full_code_context += self._mcp_awareness(project)
|
|
300
|
+
|
|
301
|
+
context_dict = {
|
|
302
|
+
"project": project,
|
|
303
|
+
"task": task,
|
|
304
|
+
"code_context": full_code_context,
|
|
305
|
+
"error_logs": allocated["error_logs"] or None,
|
|
306
|
+
"task.description": task.description,
|
|
307
|
+
"task.target_files": ", ".join(target_files)
|
|
308
|
+
if target_files
|
|
309
|
+
else "None explicitly specified",
|
|
310
|
+
"scratchpad": allocated["scratchpad"],
|
|
311
|
+
"acceptance_criteria": task.acceptance_criteria,
|
|
312
|
+
"consensus_context": allocated["consensus_context"],
|
|
313
|
+
"interface_contracts": allocated["interface_contracts"],
|
|
314
|
+
"strategy": strategy.upper(),
|
|
315
|
+
"invariants": (
|
|
316
|
+
f"Strategy: {strategy.upper()}. Output MUST be syntactically valid. "
|
|
317
|
+
"Provide certainty indicators."
|
|
318
|
+
),
|
|
319
|
+
# Marks the boundary between the cacheable stable context above
|
|
320
|
+
# and the volatile tail below (see PROMPT_TEMPLATES); the client
|
|
321
|
+
# caches everything before it.
|
|
322
|
+
"cache_breakpoint": CACHE_BREAKPOINT,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
system_prompt = prompt_manager.format_prompt("system", context_dict)
|
|
326
|
+
# Use the error-correction template whenever failures are known —
|
|
327
|
+
# including a seeded attempt 0 on a red baseline — so the model always
|
|
328
|
+
# sees the actual failures to fix rather than editing blind.
|
|
329
|
+
if error_logs:
|
|
330
|
+
prompt = prompt_manager.format_prompt(
|
|
331
|
+
"error_correction_instruction", context_dict
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
prompt = prompt_manager.format_prompt(
|
|
335
|
+
"task_completion_instruction", context_dict
|
|
336
|
+
)
|
|
337
|
+
# Tool-based edit extraction returns whole-file content via a forced
|
|
338
|
+
# tool call; the SEARCH/REPLACE contract applies only to the plain
|
|
339
|
+
# text-generation path, where the parser expects anchored hunks.
|
|
340
|
+
if not get_setting(project.config, "llm", "use_tools"):
|
|
341
|
+
# After repeated anchor-match failures, switch to a full-file
|
|
342
|
+
# rewrite (no anchoring → always applies), breaking the stall.
|
|
343
|
+
if apply_failures >= 2:
|
|
344
|
+
prompt += FULL_FILE_FALLBACK_INSTRUCTIONS
|
|
345
|
+
else:
|
|
346
|
+
prompt += EDIT_FORMAT_INSTRUCTIONS
|
|
347
|
+
|
|
348
|
+
routed_model = self._select_model(
|
|
349
|
+
project, task, strategy, attempt, max_retries
|
|
350
|
+
)
|
|
351
|
+
try:
|
|
352
|
+
with project.llm_client.track_task(task.id):
|
|
353
|
+
llm_response, aborted, pending_attempt = self._invoke_routed(
|
|
354
|
+
project,
|
|
355
|
+
task,
|
|
356
|
+
prompt,
|
|
357
|
+
system_prompt,
|
|
358
|
+
routed_model,
|
|
359
|
+
attempt,
|
|
360
|
+
track_models,
|
|
361
|
+
)
|
|
362
|
+
except Exception as e:
|
|
363
|
+
msg = f"LLM generation failed: {e}"
|
|
364
|
+
logger.error(msg)
|
|
365
|
+
self._ledger_record(
|
|
366
|
+
project,
|
|
367
|
+
task,
|
|
368
|
+
{
|
|
369
|
+
"model": routed_model
|
|
370
|
+
or getattr(project.llm_client, "model", ""),
|
|
371
|
+
"attempt": attempt,
|
|
372
|
+
"cost_before": 0.0,
|
|
373
|
+
"latency": 0.0,
|
|
374
|
+
"aborted": False,
|
|
375
|
+
},
|
|
376
|
+
success=False,
|
|
377
|
+
)
|
|
378
|
+
self._abort_task(
|
|
379
|
+
project, branch_name, base_branch, snapshot, untracked_before
|
|
380
|
+
)
|
|
381
|
+
return self._fail_task(project, task, msg)
|
|
382
|
+
|
|
383
|
+
if aborted:
|
|
384
|
+
logger.warning(
|
|
385
|
+
f"LLM stream aborted for {task.id}; retrying with stricter instruction."
|
|
386
|
+
)
|
|
387
|
+
error_logs = "ERROR: response was not code. Output ONLY file edits as code blocks with file paths."
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
certainty = CertaintyScorer.compute_score(llm_response)
|
|
391
|
+
logger.info(f"LLM Certainty Score: {certainty:.2f}")
|
|
392
|
+
|
|
393
|
+
edits, resolve_error = self._resolve_edits(project, llm_response)
|
|
394
|
+
if resolve_error:
|
|
395
|
+
apply_failures += 1
|
|
396
|
+
logger.warning(
|
|
397
|
+
f"Surgical edit could not be applied (#{apply_failures}): "
|
|
398
|
+
f"{resolve_error}"
|
|
399
|
+
)
|
|
400
|
+
# Two strikes -> next attempt is told to emit the whole file
|
|
401
|
+
# (FULL_FILE_FALLBACK_INSTRUCTIONS), which can't miss an anchor.
|
|
402
|
+
if apply_failures >= 2:
|
|
403
|
+
error_logs = (
|
|
404
|
+
"ERROR: anchored SEARCH/REPLACE edits keep failing to "
|
|
405
|
+
"match the file. Output the COMPLETE updated file instead "
|
|
406
|
+
"(full contents in one code block with the file path)."
|
|
407
|
+
)
|
|
408
|
+
else:
|
|
409
|
+
error_logs = (
|
|
410
|
+
"ERROR: a SEARCH/REPLACE edit did not apply cleanly. "
|
|
411
|
+
f"{resolve_error} Re-read the file and emit a corrected "
|
|
412
|
+
"SEARCH block that matches the current content verbatim."
|
|
413
|
+
)
|
|
414
|
+
continue
|
|
415
|
+
edits = self._validate_edit_paths(project, task, edits)
|
|
416
|
+
if not edits:
|
|
417
|
+
logger.warning("No file edits detected in LLM response.")
|
|
418
|
+
else:
|
|
419
|
+
stall_risk = self.stall_detector.push_edit(edits)
|
|
420
|
+
if stall_risk > 0.7:
|
|
421
|
+
logger.warning(f"High stall risk detected ({stall_risk:.2f}).")
|
|
422
|
+
if attempt > 1:
|
|
423
|
+
error_logs = "ERROR: Stalling detected. Try a fundamentally different approach."
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
validation_failed = False
|
|
427
|
+
for file_path, content in edits.items():
|
|
428
|
+
lang = _detect_language(file_path)
|
|
429
|
+
valid, error = CodeValidator.validate_code(content, language=lang)
|
|
430
|
+
if not valid:
|
|
431
|
+
logger.error(f"Validation failed for {file_path}: {error}")
|
|
432
|
+
error_logs = f"SYNTAX ERROR in {file_path}:\n{error}"
|
|
433
|
+
validation_failed = True
|
|
434
|
+
break
|
|
435
|
+
|
|
436
|
+
if validation_failed:
|
|
437
|
+
continue
|
|
438
|
+
|
|
439
|
+
tamper = self._detect_test_tampering(project, edits)
|
|
440
|
+
if tamper:
|
|
441
|
+
logger.error(f"Test tampering rejected: {tamper}")
|
|
442
|
+
error_logs = (
|
|
443
|
+
"ERROR: test files were weakened. Do not delete "
|
|
444
|
+
"tests, weaken assertions, or add skip/ignore markers "
|
|
445
|
+
"to make the suite pass. Fix the real code instead. "
|
|
446
|
+
f"Detected: {tamper}"
|
|
447
|
+
)
|
|
448
|
+
continue
|
|
449
|
+
|
|
450
|
+
dangling = self._detect_dangling_references(project, edits)
|
|
451
|
+
if dangling:
|
|
452
|
+
logger.warning(f"Incomplete refactor — dangling refs: {dangling}")
|
|
453
|
+
error_logs = (
|
|
454
|
+
"ERROR: this edit removed or renamed a symbol but left "
|
|
455
|
+
"references to it in files you did not change. Update EVERY "
|
|
456
|
+
"one of these call sites in this attempt (do not fix them "
|
|
457
|
+
f"one at a time): {dangling}"
|
|
458
|
+
)
|
|
459
|
+
continue
|
|
460
|
+
|
|
461
|
+
# Independent adversarial critique BEFORE applying. A rejection
|
|
462
|
+
# feeds concrete objections back as the next attempt's context
|
|
463
|
+
# (regenerate), bounded by critic_max_rejections so an
|
|
464
|
+
# over-zealous critic can't starve the loop — after that the edit
|
|
465
|
+
# flows to the authoritative build/test gates. SKIP/APPROVE fall
|
|
466
|
+
# through and apply as normal; off path is unchanged.
|
|
467
|
+
if critic_enabled and critic_rejections < critic_max_rejections:
|
|
468
|
+
verdict = self._run_edit_critic(project, task, edits)
|
|
469
|
+
if verdict.rejected:
|
|
470
|
+
critic_rejections += 1
|
|
471
|
+
logger.warning(
|
|
472
|
+
f"Adversarial critic rejected the edit on attempt "
|
|
473
|
+
f"{attempt + 1} ({critic_rejections}/"
|
|
474
|
+
f"{critic_max_rejections}): {verdict.objections}"
|
|
475
|
+
)
|
|
476
|
+
error_logs = self._build_critic_error_context(
|
|
477
|
+
verdict.objections
|
|
478
|
+
)
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
self._apply_edits(project, edits)
|
|
482
|
+
self._run_formatters(project, edits.keys())
|
|
483
|
+
edited_files.update(edits.keys())
|
|
484
|
+
|
|
485
|
+
# Whether an OBJECTIVE compile/type gate passed this attempt. A green
|
|
486
|
+
# build or typecheck IS verification, so a typecheck-only target (a
|
|
487
|
+
# frontend with no unit tests) must NOT then also be gated on the
|
|
488
|
+
# LLM's self-reported certainty — that wrongly rejects good edits.
|
|
489
|
+
gate_verified = False
|
|
490
|
+
build_cmd = task_build_command
|
|
491
|
+
if build_cmd:
|
|
492
|
+
success, output = self._run_command(
|
|
493
|
+
project, build_cmd, timeout=build_timeout, cwd=task_cwd
|
|
494
|
+
)
|
|
495
|
+
if not success:
|
|
496
|
+
logger.warning(f"Build failed on attempt {attempt + 1}")
|
|
497
|
+
locations = resolver.resolve_errors(output)
|
|
498
|
+
attributed_error = resolver.format_for_llm(locations)
|
|
499
|
+
classified = format_classified_error(output)
|
|
500
|
+
error_logs = self._build_error_context(
|
|
501
|
+
prior_errors, attempt, output, classified, attributed_error
|
|
502
|
+
)
|
|
503
|
+
continue
|
|
504
|
+
gate_verified = True
|
|
505
|
+
|
|
506
|
+
if typecheck_command:
|
|
507
|
+
success, output = self._run_command(
|
|
508
|
+
project, typecheck_command, timeout=build_timeout, cwd=task_cwd
|
|
509
|
+
)
|
|
510
|
+
if not success:
|
|
511
|
+
logger.warning(f"Type check failed on attempt {attempt + 1}")
|
|
512
|
+
locations = resolver.resolve_errors(output)
|
|
513
|
+
attributed_error = resolver.format_for_llm(locations)
|
|
514
|
+
classified = format_classified_error(output)
|
|
515
|
+
error_logs = self._build_error_context(
|
|
516
|
+
prior_errors, attempt, output, classified, attributed_error
|
|
517
|
+
)
|
|
518
|
+
continue
|
|
519
|
+
gate_verified = True
|
|
520
|
+
|
|
521
|
+
if test_command:
|
|
522
|
+
success, output = self._run_command(
|
|
523
|
+
project, test_command, timeout=test_timeout, cwd=task_cwd
|
|
524
|
+
)
|
|
525
|
+
accepted, post_failures = self._gate_accepts(
|
|
526
|
+
success, output, baseline_failures
|
|
527
|
+
)
|
|
528
|
+
if accepted:
|
|
529
|
+
if success:
|
|
530
|
+
logger.info("Tests passed successfully.")
|
|
531
|
+
else:
|
|
532
|
+
logger.info(
|
|
533
|
+
f"Suite still red ({post_failures} failing) but not "
|
|
534
|
+
f"worse than the baseline ({baseline_failures}); "
|
|
535
|
+
"accepting incremental progress."
|
|
536
|
+
)
|
|
537
|
+
if post_failures < baseline_failures:
|
|
538
|
+
# Ratchet the baseline down so a later task can't
|
|
539
|
+
# re-introduce what this one fixed.
|
|
540
|
+
project.baseline_test_failures = post_failures
|
|
541
|
+
baseline_failures = post_failures
|
|
542
|
+
acc_ok, acc_output = self._verify_acceptance(
|
|
543
|
+
project,
|
|
544
|
+
task,
|
|
545
|
+
verify_acceptance,
|
|
546
|
+
llm_acceptance_judge,
|
|
547
|
+
test_timeout,
|
|
548
|
+
cwd=task_cwd,
|
|
549
|
+
)
|
|
550
|
+
if not acc_ok:
|
|
551
|
+
logger.warning(
|
|
552
|
+
f"Acceptance criteria not met on attempt {attempt + 1}."
|
|
553
|
+
)
|
|
554
|
+
classified = format_classified_error(acc_output)
|
|
555
|
+
error_logs = self._build_acceptance_error_context(
|
|
556
|
+
prior_errors, attempt, task, classified
|
|
557
|
+
)
|
|
558
|
+
continue
|
|
559
|
+
# Spec-as-tests: the pre-written failing test must now pass.
|
|
560
|
+
spec_status, spec_detail = self._run_spec_test(
|
|
561
|
+
project, spec_test_path, test_timeout
|
|
562
|
+
)
|
|
563
|
+
if spec_status == "red" and spec_test_block:
|
|
564
|
+
logger.warning(
|
|
565
|
+
f"Spec test still fails on attempt {attempt + 1}; "
|
|
566
|
+
"implementation does not satisfy the spec."
|
|
567
|
+
)
|
|
568
|
+
error_logs = self._build_acceptance_error_context(
|
|
569
|
+
prior_errors,
|
|
570
|
+
attempt,
|
|
571
|
+
task,
|
|
572
|
+
format_classified_error(spec_detail),
|
|
573
|
+
)
|
|
574
|
+
continue
|
|
575
|
+
if spec_status == "red":
|
|
576
|
+
logger.warning(
|
|
577
|
+
f"Spec test for {task.id} still fails (advisory; not "
|
|
578
|
+
"blocking). The implementation may not fully satisfy "
|
|
579
|
+
"the acceptance criterion."
|
|
580
|
+
)
|
|
581
|
+
task.processor_data.setdefault("spec_test_gaps", []).append(
|
|
582
|
+
spec_detail[:200]
|
|
583
|
+
)
|
|
584
|
+
self._ledger_record(project, task, pending_attempt, success=True)
|
|
585
|
+
self._cache_store(
|
|
586
|
+
project,
|
|
587
|
+
system_prompt,
|
|
588
|
+
prompt,
|
|
589
|
+
llm_response,
|
|
590
|
+
pending_attempt["model"],
|
|
591
|
+
)
|
|
592
|
+
pending_attempt = None
|
|
593
|
+
self._record_success(task, target_files)
|
|
594
|
+
# Persist status BEFORE committing so the source markdown's
|
|
595
|
+
# status:completed is part of the task commit and survives the
|
|
596
|
+
# merge; otherwise the next task's checkout discards it.
|
|
597
|
+
project.task_manager.update_task_status(task.id, "completed")
|
|
598
|
+
self._commit_task(
|
|
599
|
+
project,
|
|
600
|
+
branch_name,
|
|
601
|
+
base_branch,
|
|
602
|
+
task,
|
|
603
|
+
sorted(set(target_files) | edited_files),
|
|
604
|
+
)
|
|
605
|
+
return self._complete_task(
|
|
606
|
+
project, task, "Task completed and tests passed.", output
|
|
607
|
+
)
|
|
608
|
+
else:
|
|
609
|
+
logger.warning(f"Tests failed on attempt {attempt + 1}.")
|
|
610
|
+
locations = resolver.resolve_errors(output)
|
|
611
|
+
attributed_error = resolver.format_for_llm(locations)
|
|
612
|
+
classified = format_classified_error(output)
|
|
613
|
+
error_logs = self._build_error_context(
|
|
614
|
+
prior_errors, attempt, output, classified, attributed_error
|
|
615
|
+
)
|
|
616
|
+
elif certainty < certainty_threshold and not gate_verified:
|
|
617
|
+
# No test gate AND no compile/type gate ran, so completion rests
|
|
618
|
+
# entirely on the LLM's word. With low certainty that's not enough
|
|
619
|
+
# to trust: force a retry (which escalates to surgical once
|
|
620
|
+
# attempts run out) instead of silently accepting unverified code.
|
|
621
|
+
# A green build/typecheck (gate_verified) is objective proof and
|
|
622
|
+
# bypasses this — the common frontend case (typecheck, no tests).
|
|
623
|
+
logger.warning(
|
|
624
|
+
f"No tests/build verified this and certainty {certainty:.2f} < "
|
|
625
|
+
f"{certainty_threshold:.2f}; refusing silent completion."
|
|
626
|
+
)
|
|
627
|
+
error_logs = (
|
|
628
|
+
"ERROR: no tests verify this change and the response "
|
|
629
|
+
"expressed low certainty. Provide a higher-confidence, "
|
|
630
|
+
"syntactically valid edit with explicit verification."
|
|
631
|
+
)
|
|
632
|
+
continue
|
|
633
|
+
else:
|
|
634
|
+
acc_ok, acc_output = self._verify_acceptance(
|
|
635
|
+
project,
|
|
636
|
+
task,
|
|
637
|
+
verify_acceptance,
|
|
638
|
+
llm_acceptance_judge,
|
|
639
|
+
test_timeout,
|
|
640
|
+
cwd=task_cwd,
|
|
641
|
+
)
|
|
642
|
+
if not acc_ok:
|
|
643
|
+
logger.warning(
|
|
644
|
+
f"Acceptance criteria not met on attempt {attempt + 1}."
|
|
645
|
+
)
|
|
646
|
+
classified = format_classified_error(acc_output)
|
|
647
|
+
error_logs = self._build_acceptance_error_context(
|
|
648
|
+
prior_errors, attempt, task, classified
|
|
649
|
+
)
|
|
650
|
+
continue
|
|
651
|
+
self._ledger_record(project, task, pending_attempt, success=True)
|
|
652
|
+
self._cache_store(
|
|
653
|
+
project,
|
|
654
|
+
system_prompt,
|
|
655
|
+
prompt,
|
|
656
|
+
llm_response,
|
|
657
|
+
pending_attempt["model"],
|
|
658
|
+
)
|
|
659
|
+
pending_attempt = None
|
|
660
|
+
self._record_success(task, target_files)
|
|
661
|
+
# Persist status BEFORE committing so status:completed rides into
|
|
662
|
+
# the task commit and survives the merge (see the tests-passed
|
|
663
|
+
# path above).
|
|
664
|
+
project.task_manager.update_task_status(task.id, "completed")
|
|
665
|
+
self._commit_task(
|
|
666
|
+
project,
|
|
667
|
+
branch_name,
|
|
668
|
+
base_branch,
|
|
669
|
+
task,
|
|
670
|
+
sorted(set(target_files) | edited_files),
|
|
671
|
+
)
|
|
672
|
+
return self._complete_task(
|
|
673
|
+
project, task, "Task completed (no tests run).", llm_response
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
# The final attempt fell through without succeeding; record it before
|
|
677
|
+
# escalation/failure so its model gets the failing outcome it earned.
|
|
678
|
+
if pending_attempt is not None:
|
|
679
|
+
self._ledger_record(project, task, pending_attempt, success=False)
|
|
680
|
+
pending_attempt = None
|
|
681
|
+
|
|
682
|
+
# Strategy escalation: if current strategy failed, try one more attempt
|
|
683
|
+
# with "surgical". Guarded by _depth so escalation can never recurse more
|
|
684
|
+
# than once, even if the strategy-selection logic changes later.
|
|
685
|
+
current_strategy = task.processor_data.get("strategy", "iterative")
|
|
686
|
+
if current_strategy != "surgical" and _depth < 1:
|
|
687
|
+
logger.info(
|
|
688
|
+
f"Escalating strategy from {current_strategy} to surgical for final attempt"
|
|
689
|
+
)
|
|
690
|
+
self._abort_task(
|
|
691
|
+
project, branch_name, base_branch, snapshot, untracked_before
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
task.processor_data["strategy"] = "surgical"
|
|
695
|
+
task.processor_data["invariants"] = (
|
|
696
|
+
"ESCALATED: Previous strategy failed. Use SURGICAL approach: "
|
|
697
|
+
"make the smallest possible change to fix the immediate error. "
|
|
698
|
+
"Do not refactor or restructure. Minimal, targeted fix only."
|
|
699
|
+
)
|
|
700
|
+
# Recursive single attempt with surgical strategy
|
|
701
|
+
return self.execute(
|
|
702
|
+
task, project, use_git_branch=use_git_branch, _depth=_depth + 1
|
|
703
|
+
)
|
|
704
|
+
elif _depth >= 1:
|
|
705
|
+
logger.warning(
|
|
706
|
+
f"Strategy escalation blocked at depth {_depth}: already exhausted all strategies"
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
logger.warning(
|
|
710
|
+
f"Task {task.id} failed after all attempts including escalation. Reverting."
|
|
711
|
+
)
|
|
712
|
+
self._abort_task(project, branch_name, base_branch, snapshot, untracked_before)
|
|
713
|
+
self.scratchpad.record(
|
|
714
|
+
category="pitfall",
|
|
715
|
+
discovery=(
|
|
716
|
+
f"Failed after {max_retries} attempts + escalation: "
|
|
717
|
+
f"{error_logs[:200] if error_logs else 'unknown'}"
|
|
718
|
+
),
|
|
719
|
+
task_id=task.id,
|
|
720
|
+
files=target_files,
|
|
721
|
+
)
|
|
722
|
+
return self._fail_task(
|
|
723
|
+
project,
|
|
724
|
+
task,
|
|
725
|
+
f"Task failed after {max_retries} attempts + escalation.",
|
|
726
|
+
error_logs,
|
|
727
|
+
)
|