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,387 @@
|
|
|
1
|
+
"""Task decomposition with dependency resolution, ported from /build Phase 3.
|
|
2
|
+
|
|
3
|
+
Breaks a spec into ordered, atomic tasks with dependency tracking.
|
|
4
|
+
Uses topological sort to determine execution order.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections import deque
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from misterdev.core.models import Task
|
|
11
|
+
from misterdev.llm.responses import extract_json_array
|
|
12
|
+
from misterdev.utils.file_utils import safe_ref_slug
|
|
13
|
+
from misterdev.core.planning.assessment import ProjectAssessment
|
|
14
|
+
from misterdev.core.modes import BuildMode
|
|
15
|
+
from misterdev.llm.client import BaseLLMClient
|
|
16
|
+
from misterdev.logging_setup import setup_logger
|
|
17
|
+
|
|
18
|
+
logger = setup_logger(__name__)
|
|
19
|
+
|
|
20
|
+
# /build ordering: infrastructure > core types > core logic > features >
|
|
21
|
+
# integration > tests > fixes > cleanup
|
|
22
|
+
CATEGORY_ORDER = {
|
|
23
|
+
"infrastructure": 0,
|
|
24
|
+
"core": 1,
|
|
25
|
+
"feature": 2,
|
|
26
|
+
"integration": 3,
|
|
27
|
+
"test": 4,
|
|
28
|
+
"fix": 5,
|
|
29
|
+
"docs": 6,
|
|
30
|
+
"cleanup": 7,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
MAX_TASKS = 30
|
|
34
|
+
|
|
35
|
+
DECOMPOSE_PROMPT = """You are a task decomposer for a software project.
|
|
36
|
+
|
|
37
|
+
Given the project assessment and spec below, break the work into an ordered
|
|
38
|
+
list of atomic, verifiable tasks. Each task must be completable in one editing
|
|
39
|
+
session (1-20 files) and have a concrete "done" condition.
|
|
40
|
+
|
|
41
|
+
## Project Assessment
|
|
42
|
+
|
|
43
|
+
### Structure
|
|
44
|
+
- Type: {project_type}
|
|
45
|
+
- Languages: {languages}
|
|
46
|
+
- Frameworks: {frameworks}
|
|
47
|
+
- Build: {build_command}
|
|
48
|
+
- Test: {test_command}
|
|
49
|
+
- Package manager: {package_manager}
|
|
50
|
+
|
|
51
|
+
### Health
|
|
52
|
+
- Builds: {builds}
|
|
53
|
+
- Tests pass: {tests_pass} ({test_count} tests, {test_failures} failures)
|
|
54
|
+
- Lint clean: {lint_clean}
|
|
55
|
+
|
|
56
|
+
### Features
|
|
57
|
+
- Existing: {existing_features}
|
|
58
|
+
- Incomplete: {incomplete_features}
|
|
59
|
+
- Missing: {missing_features}
|
|
60
|
+
- Broken: {broken_items}
|
|
61
|
+
- Stubs: {stub_items}
|
|
62
|
+
|
|
63
|
+
### Context
|
|
64
|
+
- Purpose: {purpose}
|
|
65
|
+
- Conventions: {conventions}
|
|
66
|
+
|
|
67
|
+
### Risk
|
|
68
|
+
- Tech debt score: {debt_score}/100
|
|
69
|
+
- Risk level: {risk_level}
|
|
70
|
+
|
|
71
|
+
### Project Files (REAL paths and their symbols — the actual code that exists)
|
|
72
|
+
{file_map}
|
|
73
|
+
{targets_section}
|
|
74
|
+
## Spec
|
|
75
|
+
{spec}
|
|
76
|
+
|
|
77
|
+
## Mode
|
|
78
|
+
{mode}
|
|
79
|
+
|
|
80
|
+
## Rules
|
|
81
|
+
- files_to_modify and context_files MUST be REAL paths taken from the Project
|
|
82
|
+
Files map above. NEVER invent a path or guess one from a feature/test name: to
|
|
83
|
+
change a function, find the file in the map that already defines it and put
|
|
84
|
+
THAT path in files_to_modify. Use files_to_create only for a file that is
|
|
85
|
+
genuinely absent from the map.
|
|
86
|
+
{targets_rule}{existing_guidance}- Max {max_tasks} tasks. Prioritize: must-fix > must-complete > should-add.
|
|
87
|
+
- Order: infrastructure > core types > core logic > features > integration > tests > fixes > cleanup.
|
|
88
|
+
- Each task's files_to_modify must not overlap with another task's files_to_create unless a dependency is declared.
|
|
89
|
+
- For DEBUG mode: order by build-blocking > test-blocking > runtime errors > warnings.
|
|
90
|
+
- For COMPLETE mode: order by infrastructure > core > features > tests > docs > cleanup.
|
|
91
|
+
- acceptance_criteria must be specific and testable (e.g., "pytest tests/test_auth.py passes" not "auth works").
|
|
92
|
+
- context_files should include imports, interfaces, or types that the task needs to read but not modify.
|
|
93
|
+
|
|
94
|
+
Return a JSON array of task objects. Each object must have exactly these fields:
|
|
95
|
+
id (string, like "T-001"),
|
|
96
|
+
title (string),
|
|
97
|
+
description (string),
|
|
98
|
+
acceptance_criteria (string),
|
|
99
|
+
files_to_create (array of strings),
|
|
100
|
+
files_to_modify (array of strings),
|
|
101
|
+
context_files (array of strings, files needed for context but not modified),
|
|
102
|
+
dependencies (array of task id strings),
|
|
103
|
+
complexity ("trivial", "small", "medium", "large", or "architectural"),
|
|
104
|
+
category ("infrastructure", "core", "feature", "integration", "test", "fix", "docs", or "cleanup")
|
|
105
|
+
|
|
106
|
+
Return ONLY the JSON array, no markdown fences or other text."""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _targets_prompt(targets: list[dict] | None) -> tuple[str, str]:
|
|
110
|
+
"""Render the optional sub-project (targets) section and grounding rule.
|
|
111
|
+
|
|
112
|
+
Returns ("", "") when there are no targets, so the single-target prompt is
|
|
113
|
+
unchanged. When present, the planner is told the sub-project boundaries and
|
|
114
|
+
instructed to keep each task within ONE target so gate routing stays clean.
|
|
115
|
+
"""
|
|
116
|
+
if not targets:
|
|
117
|
+
return "", ""
|
|
118
|
+
lines = ["\n### Sub-projects (targets — each has its OWN build/test toolchain)"]
|
|
119
|
+
for t in targets:
|
|
120
|
+
path = (t.get("path") or "").strip("/")
|
|
121
|
+
if not path:
|
|
122
|
+
continue
|
|
123
|
+
name = t.get("name") or path
|
|
124
|
+
gate = t.get("build_command") or t.get("test_command") or "?"
|
|
125
|
+
lines.append(f"- {name}: files under `{path}/` (gate: {gate})")
|
|
126
|
+
section = "\n".join(lines) + "\n"
|
|
127
|
+
rule = (
|
|
128
|
+
"- This repo has multiple sub-projects (targets above). Each task's files "
|
|
129
|
+
"MUST stay within ONE target's directory — never mix files from different "
|
|
130
|
+
"targets in a single task (they are built/tested with different "
|
|
131
|
+
"toolchains). Split cross-target work into separate, dependency-ordered "
|
|
132
|
+
"tasks (e.g. a core task, then the frontend task that consumes it).\n"
|
|
133
|
+
)
|
|
134
|
+
return section, rule
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def decompose_spec(
|
|
138
|
+
spec: str,
|
|
139
|
+
assessment: ProjectAssessment,
|
|
140
|
+
mode: BuildMode,
|
|
141
|
+
llm_client: BaseLLMClient,
|
|
142
|
+
project_ref: str,
|
|
143
|
+
max_tasks: int = MAX_TASKS,
|
|
144
|
+
file_map: str = "",
|
|
145
|
+
targets: list[dict] | None = None,
|
|
146
|
+
) -> list[Task]:
|
|
147
|
+
"""Use LLM to decompose a spec into ordered tasks with dependencies."""
|
|
148
|
+
s = assessment.structure
|
|
149
|
+
h = assessment.health
|
|
150
|
+
f = assessment.features
|
|
151
|
+
c = assessment.context
|
|
152
|
+
|
|
153
|
+
# CREATE is greenfield (files_to_create is expected); every other mode acts
|
|
154
|
+
# on an existing tree, where recreating existing files clobbers real code.
|
|
155
|
+
existing_guidance = (
|
|
156
|
+
""
|
|
157
|
+
if mode == BuildMode.CREATE
|
|
158
|
+
else (
|
|
159
|
+
"- This is an EXISTING project, not a greenfield build. Use "
|
|
160
|
+
"files_to_create ONLY for files that genuinely do not exist yet; for "
|
|
161
|
+
"any file that already exists use files_to_modify. NEVER emit tasks "
|
|
162
|
+
"that recreate existing files, modules, build config, or scaffolding.\n"
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
targets_section, targets_rule = _targets_prompt(targets)
|
|
167
|
+
|
|
168
|
+
prompt = DECOMPOSE_PROMPT.format(
|
|
169
|
+
existing_guidance=existing_guidance,
|
|
170
|
+
targets_section=targets_section,
|
|
171
|
+
targets_rule=targets_rule,
|
|
172
|
+
file_map=file_map.strip() or "(file map unavailable — infer paths cautiously)",
|
|
173
|
+
project_type=s.project_type,
|
|
174
|
+
languages=", ".join(s.languages) if s.languages else "unknown",
|
|
175
|
+
frameworks=", ".join(s.frameworks) if s.frameworks else "none",
|
|
176
|
+
build_command=s.build_command or "none",
|
|
177
|
+
test_command=s.test_command or "none",
|
|
178
|
+
package_manager=s.package_manager or "unknown",
|
|
179
|
+
builds="yes" if h.builds else "no",
|
|
180
|
+
tests_pass="yes" if h.tests_pass else "no",
|
|
181
|
+
test_count=h.test_count,
|
|
182
|
+
test_failures=h.test_failures,
|
|
183
|
+
lint_clean="yes" if h.lint_clean else "no",
|
|
184
|
+
existing_features=", ".join(fi.name for fi in f.existing)
|
|
185
|
+
if f.existing
|
|
186
|
+
else "none",
|
|
187
|
+
incomplete_features=", ".join(
|
|
188
|
+
f"{fi.name} ({fi.complexity})" for fi in f.incomplete
|
|
189
|
+
)
|
|
190
|
+
if f.incomplete
|
|
191
|
+
else "none",
|
|
192
|
+
missing_features=", ".join(f"{fi.name} ({fi.complexity})" for fi in f.missing)
|
|
193
|
+
if f.missing
|
|
194
|
+
else "none",
|
|
195
|
+
broken_items=", ".join(f.broken) if f.broken else "none",
|
|
196
|
+
stub_items=", ".join(f.stubs) if f.stubs else "none",
|
|
197
|
+
purpose=c.purpose or "unknown",
|
|
198
|
+
conventions=c.conventions or "none specified",
|
|
199
|
+
debt_score=assessment.tech_debt.score,
|
|
200
|
+
risk_level=assessment.risk.level,
|
|
201
|
+
spec=spec,
|
|
202
|
+
mode=mode.value,
|
|
203
|
+
max_tasks=max_tasks,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
logger.info("Decomposing spec into tasks via LLM...")
|
|
207
|
+
response = llm_client.generate_code(
|
|
208
|
+
prompt, "You are a precise task decomposer. Return only valid JSON."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
tasks = _parse_tasks(response, project_ref)
|
|
212
|
+
|
|
213
|
+
# Ground the plan in the real tree before anything else: a file that already
|
|
214
|
+
# exists is a modification, not a creation. Without this, COMPLETE mode on an
|
|
215
|
+
# existing project emits "create" tasks for files that exist and the executor
|
|
216
|
+
# clobbers real code (observed: a 30-task from-scratch rebuild of a mature
|
|
217
|
+
# repo). Run before dependency detection so reclassified files participate.
|
|
218
|
+
_ground_task_paths(tasks, project_ref)
|
|
219
|
+
|
|
220
|
+
# Detect implicit dependencies from file overlaps
|
|
221
|
+
_add_implicit_dependencies(tasks)
|
|
222
|
+
|
|
223
|
+
# Validate and cap
|
|
224
|
+
if len(tasks) > max_tasks:
|
|
225
|
+
logger.warning(f"LLM returned {len(tasks)} tasks, capping at {max_tasks}")
|
|
226
|
+
tasks = tasks[:max_tasks]
|
|
227
|
+
|
|
228
|
+
return tasks
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _parse_tasks(response: str, project_ref: str) -> list[Task]:
|
|
232
|
+
"""Parse LLM JSON response into Task objects."""
|
|
233
|
+
# Strip markdown fences if present
|
|
234
|
+
text = response.strip()
|
|
235
|
+
# Strip markdown fences
|
|
236
|
+
if "```" in text:
|
|
237
|
+
lines = text.split("\n")
|
|
238
|
+
lines = [ln for ln in lines if not ln.strip().startswith("```")]
|
|
239
|
+
text = "\n".join(lines).strip()
|
|
240
|
+
|
|
241
|
+
raw_tasks = extract_json_array(text, default=None)
|
|
242
|
+
if not isinstance(raw_tasks, list):
|
|
243
|
+
logger.error("No JSON task array found in LLM response")
|
|
244
|
+
return []
|
|
245
|
+
|
|
246
|
+
tasks = []
|
|
247
|
+
seen_ids: set[str] = set()
|
|
248
|
+
for i, raw in enumerate(raw_tasks):
|
|
249
|
+
try:
|
|
250
|
+
# Sanitize the LLM-supplied id (it becomes a git branch name, commit
|
|
251
|
+
# grep key, progress/contract dict key) and dependency refs with the
|
|
252
|
+
# SAME function so cross-references stay consistent. Drop duplicates
|
|
253
|
+
# so they don't collapse in dependency maps.
|
|
254
|
+
tid = safe_ref_slug(raw.get("id") or "", fallback=f"T-{i + 1:03d}")
|
|
255
|
+
if tid in seen_ids:
|
|
256
|
+
logger.warning(f"Skipping duplicate task id {tid!r}")
|
|
257
|
+
continue
|
|
258
|
+
seen_ids.add(tid)
|
|
259
|
+
deps = [
|
|
260
|
+
slug
|
|
261
|
+
for d in raw.get("dependencies", [])
|
|
262
|
+
if (slug := safe_ref_slug(d, fallback=""))
|
|
263
|
+
]
|
|
264
|
+
task = Task(
|
|
265
|
+
id=tid,
|
|
266
|
+
title=raw.get("title", ""),
|
|
267
|
+
description=raw.get("description", ""),
|
|
268
|
+
acceptance_criteria=raw.get("acceptance_criteria", ""),
|
|
269
|
+
files_to_create=raw.get("files_to_create", []),
|
|
270
|
+
files_to_modify=raw.get("files_to_modify", []),
|
|
271
|
+
context_files=raw.get("context_files", []),
|
|
272
|
+
dependencies=deps,
|
|
273
|
+
complexity=raw.get("complexity", "medium"),
|
|
274
|
+
category=raw.get("category", "feature"),
|
|
275
|
+
type="markdown_planner",
|
|
276
|
+
status="pending",
|
|
277
|
+
project_ref=project_ref,
|
|
278
|
+
processor_data=raw,
|
|
279
|
+
)
|
|
280
|
+
tasks.append(task)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.warning(f"Skipping malformed task: {e}")
|
|
283
|
+
|
|
284
|
+
return tasks
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _ground_task_paths(tasks: list[Task], project_ref: str) -> None:
|
|
288
|
+
"""Reclassify create->modify for files that already exist on disk.
|
|
289
|
+
|
|
290
|
+
The decomposing LLM, especially in COMPLETE mode on an existing project,
|
|
291
|
+
routinely lists existing files under files_to_create. Acting on that would
|
|
292
|
+
recreate (and clobber) real code. A path that already exists is a
|
|
293
|
+
modification; only genuinely absent paths stay creations.
|
|
294
|
+
"""
|
|
295
|
+
root = Path(project_ref)
|
|
296
|
+
for t in tasks:
|
|
297
|
+
still_create: list[str] = []
|
|
298
|
+
for f in t.files_to_create:
|
|
299
|
+
if f and (root / f).exists():
|
|
300
|
+
if f not in t.files_to_modify:
|
|
301
|
+
t.files_to_modify.append(f)
|
|
302
|
+
logger.warning(
|
|
303
|
+
f"Task {t.id}: '{f}' already exists; treating as modify, not create."
|
|
304
|
+
)
|
|
305
|
+
else:
|
|
306
|
+
still_create.append(f)
|
|
307
|
+
t.files_to_create = still_create
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _add_implicit_dependencies(tasks: list[Task]) -> None:
|
|
311
|
+
"""If task B modifies a file that task A creates, B depends on A."""
|
|
312
|
+
creates_map: dict[str, str] = {}
|
|
313
|
+
for t in tasks:
|
|
314
|
+
for f in t.files_to_create:
|
|
315
|
+
creates_map[f] = t.id
|
|
316
|
+
|
|
317
|
+
for t in tasks:
|
|
318
|
+
for f in t.files_to_modify:
|
|
319
|
+
creator_id = creates_map.get(f)
|
|
320
|
+
if creator_id and creator_id != t.id and creator_id not in t.dependencies:
|
|
321
|
+
t.dependencies.append(creator_id)
|
|
322
|
+
logger.info(
|
|
323
|
+
f"Implicit dependency: {t.id} depends on {creator_id} (file {f})"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def topological_sort(tasks: list[Task]) -> list[Task]:
|
|
328
|
+
"""Sort tasks respecting dependencies. Falls back to category order on ties.
|
|
329
|
+
|
|
330
|
+
Returns tasks in execution order. Raises ValueError on cycles.
|
|
331
|
+
"""
|
|
332
|
+
task_map = {t.id: t for t in tasks}
|
|
333
|
+
in_degree: dict[str, int] = {t.id: 0 for t in tasks}
|
|
334
|
+
dependents: dict[str, list[str]] = {t.id: [] for t in tasks}
|
|
335
|
+
|
|
336
|
+
for t in tasks:
|
|
337
|
+
for dep_id in t.dependencies:
|
|
338
|
+
if dep_id in task_map:
|
|
339
|
+
in_degree[t.id] += 1
|
|
340
|
+
dependents[dep_id].append(t.id)
|
|
341
|
+
|
|
342
|
+
# Seed queue with zero-dependency tasks, sorted by category order
|
|
343
|
+
queue: deque[str] = deque()
|
|
344
|
+
ready = [tid for tid, deg in in_degree.items() if deg == 0]
|
|
345
|
+
ready.sort(key=lambda tid: CATEGORY_ORDER.get(task_map[tid].category, 99))
|
|
346
|
+
queue.extend(ready)
|
|
347
|
+
|
|
348
|
+
result = []
|
|
349
|
+
while queue:
|
|
350
|
+
tid = queue.popleft()
|
|
351
|
+
result.append(task_map[tid])
|
|
352
|
+
for dep_tid in dependents[tid]:
|
|
353
|
+
in_degree[dep_tid] -= 1
|
|
354
|
+
if in_degree[dep_tid] == 0:
|
|
355
|
+
queue.append(dep_tid)
|
|
356
|
+
|
|
357
|
+
if len(result) != len(tasks):
|
|
358
|
+
# Cycle detected; append remaining tasks anyway with a warning
|
|
359
|
+
remaining = [t for t in tasks if t.id not in {r.id for r in result}]
|
|
360
|
+
logger.warning(
|
|
361
|
+
f"Dependency cycle detected involving tasks: "
|
|
362
|
+
f"{[t.id for t in remaining]}. Appending in category order."
|
|
363
|
+
)
|
|
364
|
+
remaining.sort(key=lambda t: CATEGORY_ORDER.get(t.category, 99))
|
|
365
|
+
result.extend(remaining)
|
|
366
|
+
|
|
367
|
+
return result
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def format_plan(tasks: list[Task], mode: BuildMode) -> str:
|
|
371
|
+
"""Format task list as a markdown plan table (for --dry-run)."""
|
|
372
|
+
lines = [
|
|
373
|
+
"## Build Plan\n",
|
|
374
|
+
f"**Mode**: {mode.value} | **Tasks**: {len(tasks)}\n",
|
|
375
|
+
"| # | ID | Title | Category | Complexity | Depends On | Files |",
|
|
376
|
+
"|---|------|-------|----------|------------|------------|-------|",
|
|
377
|
+
]
|
|
378
|
+
for i, t in enumerate(tasks, 1):
|
|
379
|
+
deps = ", ".join(t.dependencies) if t.dependencies else "-"
|
|
380
|
+
n_create = len(t.files_to_create)
|
|
381
|
+
n_modify = len(t.files_to_modify)
|
|
382
|
+
files = f"{n_create} create, {n_modify} modify"
|
|
383
|
+
lines.append(
|
|
384
|
+
f"| {i} | {t.id} | {t.title} | {t.category} | "
|
|
385
|
+
f"{t.complexity} | {deps} | {files} |"
|
|
386
|
+
)
|
|
387
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Metacognitive Trace Auditing and Project-Specific Learning.
|
|
2
|
+
|
|
3
|
+
Reviews build session history to generate permanent project-specific rules.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Any
|
|
9
|
+
|
|
10
|
+
from misterdev.logging_setup import setup_logger
|
|
11
|
+
from misterdev.llm.client import BaseLLMClient
|
|
12
|
+
from misterdev.llm.responses import extract_json_array
|
|
13
|
+
|
|
14
|
+
logger = setup_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SessionAuditor:
|
|
18
|
+
"""Audits task execution traces to extract 'Lessons Learned'."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, project_path: Path, llm_client: BaseLLMClient):
|
|
21
|
+
self.project_path = project_path
|
|
22
|
+
self.llm = llm_client
|
|
23
|
+
self.lessons_file = project_path / ".orchestrator" / "lessons.json"
|
|
24
|
+
|
|
25
|
+
def audit_session(
|
|
26
|
+
self,
|
|
27
|
+
completed_tasks: List[Any],
|
|
28
|
+
failed_tasks: List[Any],
|
|
29
|
+
scratchpad_content: str,
|
|
30
|
+
) -> str:
|
|
31
|
+
"""Runs the audit and returns a 'Project Logic Patch' string."""
|
|
32
|
+
logger.info("Auditing build session for metacognitive learning...")
|
|
33
|
+
|
|
34
|
+
prompt = f"""Review the following session traces for this project.
|
|
35
|
+
Identify any project-specific 'pitfalls', 'conventions', or 'requirements' that were discovered.
|
|
36
|
+
|
|
37
|
+
Successful Patterns:
|
|
38
|
+
{completed_tasks}
|
|
39
|
+
|
|
40
|
+
Failed Attempts / Stalls:
|
|
41
|
+
{failed_tasks}
|
|
42
|
+
|
|
43
|
+
Scratchpad Learnings:
|
|
44
|
+
{scratchpad_content}
|
|
45
|
+
|
|
46
|
+
Generate a list of 1-5 'Project Specific Rules' for future agents.
|
|
47
|
+
Example: 'Always run black before committing', 'The database connection must be closed manually in tests'.
|
|
48
|
+
|
|
49
|
+
Return a JSON array of strings. Return ONLY the JSON array.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
response = self.llm.generate_code(
|
|
53
|
+
prompt, "You are a senior project auditor."
|
|
54
|
+
)
|
|
55
|
+
new_rules = _extract_json_array(response)
|
|
56
|
+
if new_rules:
|
|
57
|
+
self._save_lessons(new_rules)
|
|
58
|
+
return "\n".join(f"- {r}" for r in new_rules)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error(f"Metacognitive audit failed: {e}")
|
|
61
|
+
|
|
62
|
+
return "No new lessons learned."
|
|
63
|
+
|
|
64
|
+
def _save_lessons(self, new_rules: List[str]):
|
|
65
|
+
"""Permanently appends rules to the project's lessons file."""
|
|
66
|
+
self.lessons_file.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
existing = []
|
|
68
|
+
if self.lessons_file.exists():
|
|
69
|
+
try:
|
|
70
|
+
existing = json.loads(self.lessons_file.read_text(encoding="utf-8"))
|
|
71
|
+
except (json.JSONDecodeError, OSError):
|
|
72
|
+
existing = []
|
|
73
|
+
|
|
74
|
+
# Coerce to strings before dedup: the LLM sometimes returns JSON objects
|
|
75
|
+
# instead of plain rule strings, and set()-based dedup crashes on the
|
|
76
|
+
# unhashable dicts (silently killing every audit). dict.fromkeys keeps
|
|
77
|
+
# insertion order while removing duplicates.
|
|
78
|
+
def _as_text(r):
|
|
79
|
+
return r if isinstance(r, str) else json.dumps(r, ensure_ascii=False)
|
|
80
|
+
|
|
81
|
+
merged = [_as_text(r) for r in existing] + [_as_text(r) for r in new_rules]
|
|
82
|
+
updated = list(dict.fromkeys(merged))
|
|
83
|
+
self.lessons_file.write_text(json.dumps(updated, indent=2), encoding="utf-8")
|
|
84
|
+
logger.info(f"Project logic patched with {len(new_rules)} new rules.")
|
|
85
|
+
|
|
86
|
+
def get_lessons_context(self) -> str:
|
|
87
|
+
"""Retrieves existing lessons for prompt injection."""
|
|
88
|
+
if not self.lessons_file.exists():
|
|
89
|
+
return ""
|
|
90
|
+
try:
|
|
91
|
+
rules = json.loads(self.lessons_file.read_text(encoding="utf-8"))
|
|
92
|
+
if not rules:
|
|
93
|
+
return ""
|
|
94
|
+
return "## Project-Specific Lessons (Historical)\n" + "\n".join(
|
|
95
|
+
f"- {r}" for r in rules
|
|
96
|
+
)
|
|
97
|
+
except (json.JSONDecodeError, OSError):
|
|
98
|
+
return ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _extract_json_array(response: str) -> List[str]:
|
|
102
|
+
"""Extract a JSON array from an LLM response (shared helper)."""
|
|
103
|
+
return extract_json_array(response)
|